From 6b4f3a3bde7eb9a8604617b91effe0f4a2e3921b Mon Sep 17 00:00:00 2001 From: "copilot-cli-release-app[bot]" Date: Sun, 4 Oct 2026 19:39:47 +0000 Subject: [PATCH 1/4] Update SDK snapshot for Copilot CLI 1.0.92-4 --- .../actions/download-sdk-runtime/action.yml | 34 + .github/actions/run-alpine-tests/action.yml | 14 +- .github/skills/sdk-java-e2e-test/SKILL.md | 165 +- .github/skills/sdk-java-e2e-test/examples.md | 181 - .github/workflows/sdk-dotnet.yml | 124 +- .github/workflows/sdk-go.yml | 63 +- .github/workflows/sdk-java.yml | 37 +- .github/workflows/sdk-nodejs.yml | 98 +- .github/workflows/sdk-python.yml | 57 +- .github/workflows/sdk-runtime-artifact.yml | 99 +- .github/workflows/sdk-rust.yml | 102 +- .github/workflows/sdk.yml | 5 + CONTRIBUTING.md | 58 +- docs/features/fleet-mode.md | 6 +- docs/features/streaming-events.md | 26 +- docs/getting-started.md | 8 + docs/observability/opentelemetry.md | 47 + dotnet/README.md | 39 + dotnet/src/Canvas.cs | 6 + dotnet/src/Client.cs | 32 +- dotnet/src/Generated/Rpc.cs | 6385 ++++++++++------- dotnet/src/Generated/SessionEvents.cs | 455 +- dotnet/src/Session.cs | 25 + dotnet/src/SessionFsProvider.cs | 127 +- dotnet/src/Types.cs | 173 +- dotnet/src/build/GitHub.Copilot.SDK.targets | 4 +- dotnet/test/E2E/CompactionE2ETests.cs | 15 +- .../E2E/HookLifecycleAndOutputE2ETests.cs | 5 +- dotnet/test/E2E/PermissionE2ETests.cs | 2 + dotnet/test/E2E/RpcShellAndFleetE2ETests.cs | 2 +- ...cenarioTestingJsExtensionBridgeE2ETests.cs | 31 +- dotnet/test/E2E/SessionFsE2ETests.cs | 82 +- dotnet/test/E2E/SubagentHooksE2ETests.cs | 92 +- dotnet/test/E2E/ToolsE2ETests.cs | 72 + dotnet/test/Harness/ReplayProxy.cs | 62 +- dotnet/test/Harness/TestHelper.cs | 61 +- .../test/Unit/ClientSessionLifetimeTests.cs | 301 +- dotnet/test/Unit/E2ETestFixtureTests.cs | 34 + .../Unit/ExtensionLaunchDiagnosticsTests.cs | 45 + dotnet/test/Unit/JsonRpcTests.cs | 23 + dotnet/test/Unit/MSBuildTargetsTests.cs | 6 + dotnet/test/Unit/ReplayProxyTests.cs | 43 + dotnet/test/Unit/SetToolsTests.cs | 251 +- go/README.md | 44 + go/canvas.go | 5 + go/client.go | 63 +- go/client_test.go | 245 + go/cmd/bundler/main.go | 2 +- go/cmd/bundler/main_test.go | 3 +- go/internal/e2e/compaction_e2e_test.go | 37 +- go/internal/e2e/hooks_extended_e2e_test.go | 8 +- go/internal/e2e/session_fs_e2e_test.go | 104 + go/internal/e2e/subagent_hooks_e2e_test.go | 154 +- go/internal/e2e/testharness/proxy.go | 15 +- go/internal/e2e/testharness/proxy_test.go | 53 + go/internal/e2e/tools_e2e_test.go | 49 + go/rpc/zrpc.go | 1111 ++- go/rpc/zrpc_encoding.go | 253 +- go/rpc/zsession_encoding.go | 116 + go/rpc/zsession_events.go | 221 +- go/session.go | 20 + go/session_fs_provider.go | 83 + go/session_fs_provider_test.go | 64 + go/subagent_hooks_test.go | 131 + go/types.go | 106 +- go/zsession_events.go | 336 +- java/AGENTS.md | 2 +- java/README.md | 64 + java/copilot-native/scripts/fetch-native.mjs | 1 + .../scripts/fetch-native.test.mjs | 2 + java/scripts/codegen/java.test.ts | 283 + java/scripts/codegen/java.ts | 239 +- .../AssistantMessageReasoningBlocks.java | 4 +- .../copilot/generated/HumanResponseActor.java | 37 + .../generated/HumanResponseRecordedEvent.java | 47 + .../HumanResponseRecordedResponse.java | 36 + .../HumanResponseRecordedResponseAskUser.java | 52 + ...nResponseRecordedResponseExitPlanMode.java | 87 + ...umanResponseRecordedResponseUserInput.java | 66 + .../copilot/generated/SessionEvent.java | 4 + .../generated/SessionModelChangeEvent.java | 2 + .../copilot/generated/SessionResumeEvent.java | 2 + .../copilot/generated/SessionStartEvent.java | 2 + .../generated/ToolExecutionCompleteEvent.java | 4 + .../ToolExecutionCompleteFileEdit.java | 33 + .../ToolExecutionCompleteFileEditKind.java | 53 + .../ToolExecutionPartialResultEvent.java | 6 +- .../generated/ToolShellOutputEvent.java | 47 + .../generated/ToolShellOutputStream.java | 37 + .../rpc/AccountsAcquireEntraTokenParams.java | 41 - .../copilot/generated/rpc/CurrentModel.java | 2 + ...t.java => CustomizationReloadOutcome.java} | 30 +- .../rpc/CustomizationReloadStatus.java | 51 + .../rpc/CustomizationReloadSubsystem.java | 66 + .../rpc/CustomizationsReloadResult.java | 35 + .../generated/rpc/DiscoveredModel.java | 49 + .../rpc/EntraTokenAcquireResultOk.java | 51 - .../generated/rpc/ModelArtifactDetails.java | 44 + .../rpc/ModelCapabilitiesSupports.java | 2 + .../rpc/ModelProviderAdapterDescriptor.java | 42 + ...delProviderAdapterOperationDescriptor.java | 33 + .../rpc/ModelProviderAttribution.java | 35 + .../ModelProviderAutomaticDiscoveryMode.java | 37 + ...ModelProviderAutomaticDiscoveryPolicy.java | 37 + ...ModelProviderConfigurationDisposition.java | 35 + .../ModelProviderDiscoveryNetworkScope.java | 41 + .../generated/rpc/ModelProviderInstance.java | 43 + .../rpc/ModelProviderInstanceReference.java | 37 + .../rpc/ModelProviderOperationOutcome.java | 33 + .../ModelProviderOperationOutcomeCode.java | 39 + .../rpc/ModelProviderProvenance.java | 39 + .../rpc/ModelProviderProvenanceSource.java | 39 + .../generated/rpc/ModelProviderWarning.java | 33 + .../rpc/SandboxProxyCaCreateParams.java | 30 + .../rpc/SandboxProxyCaCreateResult.java | 30 + .../rpc/SandboxProxyCaGetStatusParams.java | 30 + .../rpc/SandboxProxyCaGetStatusResult.java | 34 + .../rpc/SandboxProxyCaRemoveResult.java | 34 + .../rpc/SandboxProxyCaRotateParams.java | 30 + .../rpc/SandboxProxyCaRotateResult.java | 34 + ...eraction.java => SandboxProxyCaState.java} | 26 +- .../rpc/SandboxProxyCaTrustParams.java | 30 + .../rpc/SandboxProxyCaTrustResult.java | 34 + .../generated/rpc/ServerAccountsApi.java | 40 - .../copilot/generated/rpc/ServerRpc.java | 3 - .../generated/rpc/ServerSandboxApi.java | 4 + .../rpc/ServerSandboxProxyCaApi.java | 84 + .../rpc/SessionCustomizationsApi.java | 4 +- .../copilot/generated/rpc/SessionFsError.java | 4 +- .../rpc/SessionFsReadFileBytesParams.java | 32 + .../rpc/SessionFsReadFileBytesResult.java | 32 + .../rpc/SessionFsSetProviderCapabilities.java | 15 +- .../rpc/SessionFsWriteFileBytesParams.java | 36 + .../generated/rpc/SessionInstructionsApi.java | 4 +- .../rpc/SessionModelGetCurrentResult.java | 2 + .../generated/rpc/SessionProvidersApi.java | 80 + .../rpc/SessionProvidersDiscoverParams.java | 34 + .../rpc/SessionProvidersDiscoverResult.java | 33 + ... => SessionProvidersGetCatalogParams.java} | 20 +- ... => SessionProvidersGetCatalogResult.java} | 10 +- .../rpc/SessionProvidersGetStatusParams.java | 32 + .../rpc/SessionProvidersGetStatusResult.java | 36 + .../rpc/SessionProvidersModelsApi.java | 65 + .../rpc/SessionProvidersModelsListParams.java | 32 + .../rpc/SessionProvidersModelsListResult.java | 33 + ...idersModelsPrepareConfigurationParams.java | 34 + ...idersModelsPrepareConfigurationResult.java | 41 + .../copilot/generated/rpc/SessionRpc.java | 3 + .../copilot/generated/rpc/SessionUiApi.java | 48 + .../SessionUiHandleHumanAskUserParams.java | 34 + ...essionUiHandleHumanExitPlanModeParams.java | 34 + .../SessionUiHandleHumanUserInputParams.java | 34 + .../generated/rpc/UIElicitationResult.java | 27 + .../generated/rpc/UIHandlePendingResult.java | 27 + .../com/github/copilot/CopilotSession.java | 22 + .../com/github/copilot/rpc/SessionHooks.java | 46 +- .../copilot/rpc/SubagentStartHandler.java | 25 + .../copilot/rpc/SubagentStartHookInput.java | 34 + .../copilot/rpc/SubagentStartHookOutput.java | 19 + .../copilot/rpc/SubagentStopHandler.java | 26 + .../copilot/rpc/SubagentStopHookInput.java | 44 + .../copilot/rpc/SubagentStopHookOutput.java | 26 + .../copilot/rpc/SystemMessageSections.java | 9 +- .../copilot/SessionEventHandlingTest.java | 4 +- .../github/copilot/SessionHandlerTest.java | 120 + .../github/copilot/StringSchemaToolsIT.java | 49 + .../github/copilot/SubagentHooksE2ETest.java | 182 +- .../GeneratedTypesJacksonRoundTripTest.java | 16 + .../rpc/CustomizationReloadOutcomeTest.java | 33 + .../rpc/GeneratedRpcApiCoverageTest.java | 4 +- .../rpc/GeneratedRpcRecordsCoverageTest.java | 54 +- nodejs/README.md | 49 + nodejs/docs/agent-author.md | 31 +- nodejs/esbuild-copilotsdk-nodejs.ts | 2 +- nodejs/package.json | 3 +- nodejs/src/canvas.ts | 7 +- nodejs/src/cliVersion.ts | 2 +- nodejs/src/client.ts | 30 +- nodejs/src/generated/rpc.ts | 1647 +++-- nodejs/src/generated/session-events.ts | 247 +- nodejs/src/index.ts | 7 + nodejs/src/runtimeArtifacts.ts | 1 + nodejs/src/session.ts | 26 +- nodejs/src/sessionFsProvider.ts | 69 +- nodejs/src/types.ts | 77 +- nodejs/test/client.test.ts | 257 + nodejs/test/csharp-codegen.test.ts | 55 + nodejs/test/e2e/anthropic_replay.e2e.test.ts | 206 + nodejs/test/e2e/auto_tier.e2e.test.ts | 8 +- nodejs/test/e2e/builtin_tools.e2e.test.ts | 79 +- nodejs/test/e2e/client_options.e2e.test.ts | 8 +- nodejs/test/e2e/compaction.e2e.test.ts | 22 +- .../e2e/copilot_request_handler.e2e.test.ts | 499 +- .../e2e/customizations_reload.e2e.test.ts | 58 + .../test/e2e/disabled_mcp_servers.e2e.test.ts | 3 + nodejs/test/e2e/harness/CapiProxy.test.ts | 8 +- nodejs/test/e2e/harness/CapiProxy.ts | 9 +- .../test/e2e/harness/sdkTestContext.test.ts | 52 + nodejs/test/e2e/harness/sdkTestContext.ts | 38 +- nodejs/test/e2e/harness/testBackend.ts | 49 + .../e2e/mcp_instruction_recovery.e2e.test.ts | 252 + .../e2e/mcp_legacy_sse_recovery.e2e.test.ts | 422 ++ nodejs/test/e2e/mode_handlers.e2e.test.ts | 415 +- .../test/e2e/model_effort_resume.e2e.test.ts | 221 + nodejs/test/e2e/multi-client.e2e.test.ts | 6 +- .../test/e2e/pending_work_resume.e2e.test.ts | 22 +- nodejs/test/e2e/permissions.e2e.test.ts | 11 +- .../e2e/rpc_session_state_extras.e2e.test.ts | 6 +- .../e2e/rpc_shell_user_requested.e2e.test.ts | 121 +- .../test/e2e/rpc_surface_coverage.e2e.test.ts | 54 +- .../e2e/rpc_tasks_and_handlers.e2e.test.ts | 243 +- nodejs/test/e2e/sandbox_devices.e2e.test.ts | 86 + .../test/e2e/sandbox_filesystem.e2e.test.ts | 62 + nodejs/test/e2e/sandbox_gh_config.e2e.test.ts | 138 + nodejs/test/e2e/session.e2e.test.ts | 51 +- nodejs/test/e2e/session_config.e2e.test.ts | 240 +- nodejs/test/e2e/session_fs.e2e.test.ts | 78 +- .../test/e2e/streaming_fidelity.e2e.test.ts | 26 +- nodejs/test/e2e/structured_output.e2e.test.ts | 58 +- nodejs/test/e2e/subagent_hooks.e2e.test.ts | 278 +- nodejs/test/e2e/suspend.e2e.test.ts | 6 +- .../e2e/system_message_sections.e2e.test.ts | 170 +- .../e2e/system_message_transform.e2e.test.ts | 24 +- nodejs/test/e2e/telemetry-skills.e2e.test.ts | 470 ++ nodejs/test/e2e/telemetry.e2e.test.ts | 39 +- nodejs/test/e2e/tools.e2e.test.ts | 145 +- .../e2e/tools_anthropic_updates.e2e.test.ts | 411 ++ nodejs/test/e2e/ui_elicitation.e2e.test.ts | 69 + .../test/external-tool-cancellation.test.ts | 109 + nodejs/test/package-sdk.test.ts | 16 +- nodejs/test/runtimeArtifacts.test.ts | 2 + nodejs/test/rust-codegen.test.ts | 123 +- nodejs/test/session-event-codegen.test.ts | 67 +- nodejs/test/session-event-types.test.ts | 17 + nodejs/test/session_fs_adapter.test.ts | 99 +- nodejs/test/shared-codegen.test.ts | 33 + nodejs/test/testBackend.test.ts | 74 + nodejs/test/typescript-codegen.test.ts | 45 +- nodejs/vitest.config.ts | 21 + package.json | 1 + python/README.md | 67 +- python/copilot/__init__.py | 16 + python/copilot/_cli_download.py | 1 + python/copilot/canvas.py | 9 +- python/copilot/client.py | 71 +- python/copilot/generated/rpc.py | 2400 +++++-- python/copilot/generated/session_events.py | 321 +- python/copilot/session.py | 75 +- python/copilot/session_fs_provider.py | 61 +- python/copilot/tools.py | 4 +- python/e2e/test_compaction_e2e.py | 25 +- python/e2e/test_pending_work_resume_e2e.py | 36 +- python/e2e/test_session_fs_e2e.py | 66 +- python/e2e/test_subagent_hooks_e2e.py | 212 +- python/e2e/test_tools_e2e.py | 29 +- python/test_cli_download.py | 2 + python/test_client.py | 219 + python/test_event_forward_compatibility.py | 25 + python/test_rpc_generated.py | 27 +- python/test_session_fs_provider.py | 41 + python/test_subagent_hooks.py | 130 + python/test_tools.py | 25 +- rust/AGENTS.md | 10 +- rust/README.md | 42 +- rust/build/in_process.rs | 1 + rust/src/cache_paths.rs | 50 +- rust/src/cache_paths/tests.rs | 52 + rust/src/canvas.rs | 134 +- rust/src/canvas/tests.rs | 131 + .../http_response_reader.rs | 178 +- .../http_response_reader/tests.rs | 179 + rust/src/embeddedcli.rs | 816 +-- rust/src/embeddedcli/tests.rs | 818 +++ rust/src/ffi.rs | 149 +- rust/src/ffi/tests.rs | 152 + rust/src/generated/api_types.rs | 1731 ++++- rust/src/generated/rpc.rs | 526 +- rust/src/generated/session_events.rs | 260 +- rust/src/github_token.rs | 31 +- rust/src/github_token/tests.rs | 34 + rust/src/handler.rs | 95 +- rust/src/handler/tests.rs | 98 + rust/src/hooks.rs | 537 +- rust/src/hooks/tests.rs | 530 ++ rust/src/jsonrpc.rs | 159 +- rust/src/jsonrpc/tests.rs | 162 + rust/src/lib.rs | 862 +-- rust/src/mode.rs | 268 +- rust/src/mode/tests.rs | 271 + rust/src/permission.rs | 151 +- rust/src/permission/tests.rs | 153 + rust/src/resolve.rs | 25 +- rust/src/resolve/tests.rs | 28 + rust/src/router.rs | 33 +- rust/src/router/tests.rs | 36 + rust/src/session.rs | 324 +- rust/src/session/tests.rs | 302 + rust/src/session_fs.rs | 106 +- rust/src/session_fs/tests.rs | 49 + rust/src/session_fs/write_failure_tests.rs | 26 + rust/src/session_fs_dispatch.rs | 118 +- rust/src/session_fs_dispatch/tests.rs | 26 + rust/src/startup_timings.rs | 25 +- rust/src/startup_timings/tests.rs | 28 + rust/src/tests.rs | 813 +++ rust/src/tool.rs | 421 +- rust/src/tool/tests.rs | 422 ++ rust/src/trace_context.rs | 29 +- rust/src/trace_context/tests.rs | 32 + rust/src/transforms.rs | 112 +- rust/src/transforms/tests.rs | 115 + rust/src/types.rs | 2242 +----- rust/src/types/is_terminal_tests.rs | 49 + rust/src/types/permission_builder_tests.rs | 259 + rust/src/types/tests.rs | 1927 +++++ rust/tests/api_types_test.rs | 30 + rust/tests/e2e/session_fs.rs | 323 + rust/tests/e2e/subagent_hooks.rs | 118 +- rust/tests/e2e/tools.rs | 55 +- rust/tests/session_events_test.rs | 107 +- rust/tests/session_test.rs | 135 +- scripts/build-prerequisites.mjs | 13 +- scripts/build-prerequisites.test.mjs | 71 + scripts/ci/check-generated.mjs | 2 +- scripts/ci/check-generated.test.mjs | 1 + scripts/ci/install-dependencies.test.mjs | 185 + scripts/ci/run-dotnet-tests.sh | 3 +- scripts/ci/run-dotnet-tests.test.mjs | 14 +- scripts/ci/run-tasks.test.mjs | 100 + scripts/ci/runtime-artifact.mjs | 2 + scripts/ci/runtime-artifact.test.mjs | 3 + scripts/codegen/csharp.ts | 7 + scripts/codegen/go.ts | 41 +- scripts/codegen/python.ts | 47 +- scripts/codegen/rust.ts | 111 +- scripts/codegen/typescript.ts | 14 +- scripts/install-dependencies.mjs | 36 + scripts/run-tasks.mjs | 20 +- test/harness/modelProtocolAdapters.test.ts | 34 + test/harness/replayingCapiProxy.test.ts | 1032 ++- test/harness/replayingCapiProxy.ts | 351 +- test/harness/responsesApiAdapter.ts | 34 +- test/harness/subagentHooksReplay.test.ts | 106 +- ...thout_withdrawing_peer_tools_mid_turn.yaml | 47 + ...thout_withdrawing_peer_tools_mid_turn.yaml | 36 + ...ould_wait_for_slow_permission_handler.yaml | 3 +- ...odel_and_effort_when_started_over_rpc.yaml | 10 + ...ay_name_effort_in_a_model_issued_task.yaml | 31 + ...dbox_bypass_for_standard_device_paths.yaml | 25 + ...nly_in_the_binary_session_fs_provider.yaml | 24 + ..._use_sql_tool_via_inherited_sessionfs.yaml | 4 +- ..._through_the_sessionfs_sqlite_handler.yaml | 8 +- ...apply_subagent_lifecycle_hook_outputs.yaml | 420 ++ ...ooluse_hooks_for_sub_agent_tool_calls.yaml | 69 + ...hout_fabricating_an_execute_tool_span.yaml | 12 + ...source_and_script_facts_capture_false.yaml | 173 + ...esource_and_script_facts_capture_true.yaml | 166 + ..._zero_and_nonzero_exits_capture_false.yaml | 43 + ...y_zero_and_nonzero_exits_capture_true.yaml | 43 + ...ld_attribution_separate_capture_false.yaml | 243 + ...ild_attribution_separate_capture_true.yaml | 217 + ..._apply_patch_string_schema_to_the_sdk.yaml | 10 + ...pply_patch_override_binds_patch_input.yaml | 22 + ...ttles_without_trusted_human_authority.yaml | 23 + 364 files changed, 37061 insertions(+), 12498 deletions(-) create mode 100644 .github/actions/download-sdk-runtime/action.yml delete mode 100644 .github/skills/sdk-java-e2e-test/examples.md create mode 100644 dotnet/test/Unit/ReplayProxyTests.cs create mode 100644 go/internal/e2e/testharness/proxy_test.go create mode 100644 go/session_fs_provider_test.go create mode 100644 go/subagent_hooks_test.go create mode 100644 java/sdk/src/generated/java/com/github/copilot/generated/HumanResponseActor.java create mode 100644 java/sdk/src/generated/java/com/github/copilot/generated/HumanResponseRecordedEvent.java create mode 100644 java/sdk/src/generated/java/com/github/copilot/generated/HumanResponseRecordedResponse.java create mode 100644 java/sdk/src/generated/java/com/github/copilot/generated/HumanResponseRecordedResponseAskUser.java create mode 100644 java/sdk/src/generated/java/com/github/copilot/generated/HumanResponseRecordedResponseExitPlanMode.java create mode 100644 java/sdk/src/generated/java/com/github/copilot/generated/HumanResponseRecordedResponseUserInput.java create mode 100644 java/sdk/src/generated/java/com/github/copilot/generated/ToolExecutionCompleteFileEdit.java create mode 100644 java/sdk/src/generated/java/com/github/copilot/generated/ToolExecutionCompleteFileEditKind.java create mode 100644 java/sdk/src/generated/java/com/github/copilot/generated/ToolShellOutputEvent.java create mode 100644 java/sdk/src/generated/java/com/github/copilot/generated/ToolShellOutputStream.java delete mode 100644 java/sdk/src/generated/java/com/github/copilot/generated/rpc/AccountsAcquireEntraTokenParams.java rename java/sdk/src/generated/java/com/github/copilot/generated/rpc/{EntraTokenAcquireResult.java => CustomizationReloadOutcome.java} (54%) create mode 100644 java/sdk/src/generated/java/com/github/copilot/generated/rpc/CustomizationReloadStatus.java create mode 100644 java/sdk/src/generated/java/com/github/copilot/generated/rpc/CustomizationReloadSubsystem.java create mode 100644 java/sdk/src/generated/java/com/github/copilot/generated/rpc/CustomizationsReloadResult.java create mode 100644 java/sdk/src/generated/java/com/github/copilot/generated/rpc/DiscoveredModel.java delete mode 100644 java/sdk/src/generated/java/com/github/copilot/generated/rpc/EntraTokenAcquireResultOk.java create mode 100644 java/sdk/src/generated/java/com/github/copilot/generated/rpc/ModelArtifactDetails.java create mode 100644 java/sdk/src/generated/java/com/github/copilot/generated/rpc/ModelProviderAdapterDescriptor.java create mode 100644 java/sdk/src/generated/java/com/github/copilot/generated/rpc/ModelProviderAdapterOperationDescriptor.java create mode 100644 java/sdk/src/generated/java/com/github/copilot/generated/rpc/ModelProviderAttribution.java create mode 100644 java/sdk/src/generated/java/com/github/copilot/generated/rpc/ModelProviderAutomaticDiscoveryMode.java create mode 100644 java/sdk/src/generated/java/com/github/copilot/generated/rpc/ModelProviderAutomaticDiscoveryPolicy.java create mode 100644 java/sdk/src/generated/java/com/github/copilot/generated/rpc/ModelProviderConfigurationDisposition.java create mode 100644 java/sdk/src/generated/java/com/github/copilot/generated/rpc/ModelProviderDiscoveryNetworkScope.java create mode 100644 java/sdk/src/generated/java/com/github/copilot/generated/rpc/ModelProviderInstance.java create mode 100644 java/sdk/src/generated/java/com/github/copilot/generated/rpc/ModelProviderInstanceReference.java create mode 100644 java/sdk/src/generated/java/com/github/copilot/generated/rpc/ModelProviderOperationOutcome.java create mode 100644 java/sdk/src/generated/java/com/github/copilot/generated/rpc/ModelProviderOperationOutcomeCode.java create mode 100644 java/sdk/src/generated/java/com/github/copilot/generated/rpc/ModelProviderProvenance.java create mode 100644 java/sdk/src/generated/java/com/github/copilot/generated/rpc/ModelProviderProvenanceSource.java create mode 100644 java/sdk/src/generated/java/com/github/copilot/generated/rpc/ModelProviderWarning.java create mode 100644 java/sdk/src/generated/java/com/github/copilot/generated/rpc/SandboxProxyCaCreateParams.java create mode 100644 java/sdk/src/generated/java/com/github/copilot/generated/rpc/SandboxProxyCaCreateResult.java create mode 100644 java/sdk/src/generated/java/com/github/copilot/generated/rpc/SandboxProxyCaGetStatusParams.java create mode 100644 java/sdk/src/generated/java/com/github/copilot/generated/rpc/SandboxProxyCaGetStatusResult.java create mode 100644 java/sdk/src/generated/java/com/github/copilot/generated/rpc/SandboxProxyCaRemoveResult.java create mode 100644 java/sdk/src/generated/java/com/github/copilot/generated/rpc/SandboxProxyCaRotateParams.java create mode 100644 java/sdk/src/generated/java/com/github/copilot/generated/rpc/SandboxProxyCaRotateResult.java rename java/sdk/src/generated/java/com/github/copilot/generated/rpc/{EntraTokenInteraction.java => SandboxProxyCaState.java} (57%) create mode 100644 java/sdk/src/generated/java/com/github/copilot/generated/rpc/SandboxProxyCaTrustParams.java create mode 100644 java/sdk/src/generated/java/com/github/copilot/generated/rpc/SandboxProxyCaTrustResult.java delete mode 100644 java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerAccountsApi.java create mode 100644 java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerSandboxProxyCaApi.java create mode 100644 java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionFsReadFileBytesParams.java create mode 100644 java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionFsReadFileBytesResult.java create mode 100644 java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionFsWriteFileBytesParams.java create mode 100644 java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionProvidersApi.java create mode 100644 java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionProvidersDiscoverParams.java create mode 100644 java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionProvidersDiscoverResult.java rename java/sdk/src/generated/java/com/github/copilot/generated/rpc/{EntraTokenAcquireResultInteractionRequired.java => SessionProvidersGetCatalogParams.java} (70%) rename java/sdk/src/generated/java/com/github/copilot/generated/rpc/{SessionCustomizationsReloadResult.java => SessionProvidersGetCatalogResult.java} (68%) create mode 100644 java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionProvidersGetStatusParams.java create mode 100644 java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionProvidersGetStatusResult.java create mode 100644 java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionProvidersModelsApi.java create mode 100644 java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionProvidersModelsListParams.java create mode 100644 java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionProvidersModelsListResult.java create mode 100644 java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionProvidersModelsPrepareConfigurationParams.java create mode 100644 java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionProvidersModelsPrepareConfigurationResult.java create mode 100644 java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionUiHandleHumanAskUserParams.java create mode 100644 java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionUiHandleHumanExitPlanModeParams.java create mode 100644 java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionUiHandleHumanUserInputParams.java create mode 100644 java/sdk/src/generated/java/com/github/copilot/generated/rpc/UIElicitationResult.java create mode 100644 java/sdk/src/generated/java/com/github/copilot/generated/rpc/UIHandlePendingResult.java create mode 100644 java/sdk/src/main/java/com/github/copilot/rpc/SubagentStartHandler.java create mode 100644 java/sdk/src/main/java/com/github/copilot/rpc/SubagentStartHookInput.java create mode 100644 java/sdk/src/main/java/com/github/copilot/rpc/SubagentStartHookOutput.java create mode 100644 java/sdk/src/main/java/com/github/copilot/rpc/SubagentStopHandler.java create mode 100644 java/sdk/src/main/java/com/github/copilot/rpc/SubagentStopHookInput.java create mode 100644 java/sdk/src/main/java/com/github/copilot/rpc/SubagentStopHookOutput.java create mode 100644 java/sdk/src/test/java/com/github/copilot/StringSchemaToolsIT.java create mode 100644 java/sdk/src/test/java/com/github/copilot/generated/rpc/CustomizationReloadOutcomeTest.java create mode 100644 nodejs/test/e2e/anthropic_replay.e2e.test.ts create mode 100644 nodejs/test/e2e/customizations_reload.e2e.test.ts create mode 100644 nodejs/test/e2e/harness/sdkTestContext.test.ts create mode 100644 nodejs/test/e2e/harness/testBackend.ts create mode 100644 nodejs/test/e2e/mcp_instruction_recovery.e2e.test.ts create mode 100644 nodejs/test/e2e/mcp_legacy_sse_recovery.e2e.test.ts create mode 100644 nodejs/test/e2e/model_effort_resume.e2e.test.ts create mode 100644 nodejs/test/e2e/sandbox_devices.e2e.test.ts create mode 100644 nodejs/test/e2e/sandbox_filesystem.e2e.test.ts create mode 100644 nodejs/test/e2e/sandbox_gh_config.e2e.test.ts create mode 100644 nodejs/test/e2e/telemetry-skills.e2e.test.ts create mode 100644 nodejs/test/e2e/tools_anthropic_updates.e2e.test.ts create mode 100644 nodejs/test/testBackend.test.ts create mode 100644 python/test_session_fs_provider.py create mode 100644 python/test_subagent_hooks.py create mode 100644 rust/src/cache_paths/tests.rs create mode 100644 rust/src/canvas/tests.rs create mode 100644 rust/src/copilot_request_handler/http_response_reader/tests.rs create mode 100644 rust/src/embeddedcli/tests.rs create mode 100644 rust/src/ffi/tests.rs create mode 100644 rust/src/github_token/tests.rs create mode 100644 rust/src/handler/tests.rs create mode 100644 rust/src/hooks/tests.rs create mode 100644 rust/src/jsonrpc/tests.rs create mode 100644 rust/src/mode/tests.rs create mode 100644 rust/src/permission/tests.rs create mode 100644 rust/src/resolve/tests.rs create mode 100644 rust/src/router/tests.rs create mode 100644 rust/src/session/tests.rs create mode 100644 rust/src/session_fs/tests.rs create mode 100644 rust/src/session_fs/write_failure_tests.rs create mode 100644 rust/src/session_fs_dispatch/tests.rs create mode 100644 rust/src/startup_timings/tests.rs create mode 100644 rust/src/tests.rs create mode 100644 rust/src/tool/tests.rs create mode 100644 rust/src/trace_context/tests.rs create mode 100644 rust/src/transforms/tests.rs create mode 100644 rust/src/types/is_terminal_tests.rs create mode 100644 rust/src/types/permission_builder_tests.rs create mode 100644 rust/src/types/tests.rs create mode 100644 scripts/ci/install-dependencies.test.mjs create mode 100644 scripts/install-dependencies.mjs create mode 100644 test/snapshots/mcp_instruction_recovery/should_recover__bluebird__instructions_without_withdrawing_peer_tools_mid_turn.yaml create mode 100644 test/snapshots/mcp_instruction_recovery/should_recover__workiq__instructions_without_withdrawing_peer_tools_mid_turn.yaml create mode 100644 test/snapshots/rpc_tasks_and_handlers/should_apply_a_custom_agent_s_composed_model_and_effort_when_started_over_rpc.yaml create mode 100644 test/snapshots/rpc_tasks_and_handlers/should_preserve_a_composed_display_name_effort_in_a_model_issued_task.yaml create mode 100644 test/snapshots/sandbox_devices/does_not_offer_a_sandbox_bypass_for_standard_device_paths.yaml create mode 100644 test/snapshots/session_fs/should_view_an_image_that_exists_only_in_the_binary_session_fs_provider.yaml create mode 100644 test/snapshots/subagent_hooks/should_apply_subagent_lifecycle_hook_outputs.yaml create mode 100644 test/snapshots/telemetry_skills/emits_an_invocation_receipt_without_fabricating_an_execute_tool_span.yaml create mode 100644 test/snapshots/telemetry_skills/exports_loader_resource_and_script_facts_capture_false.yaml create mode 100644 test/snapshots/telemetry_skills/exports_loader_resource_and_script_facts_capture_true.yaml create mode 100644 test/snapshots/telemetry_skills/exports_unknown_skill_and_ordinary_zero_and_nonzero_exits_capture_false.yaml create mode 100644 test/snapshots/telemetry_skills/exports_unknown_skill_and_ordinary_zero_and_nonzero_exits_capture_true.yaml create mode 100644 test/snapshots/telemetry_skills/keeps_reloaded_skill_provenance_and_child_attribution_separate_capture_false.yaml create mode 100644 test/snapshots/telemetry_skills/keeps_reloaded_skill_provenance_and_child_attribution_separate_capture_true.yaml create mode 100644 test/snapshots/tools/reports_rejected_apply_patch_string_schema_to_the_sdk.yaml create mode 100644 test/snapshots/tools/string_schema_apply_patch_override_binds_patch_input.yaml create mode 100644 test/snapshots/ui_elicitation/automated_sdk_ask_user_callback_settles_without_trusted_human_authority.yaml diff --git a/.github/actions/download-sdk-runtime/action.yml b/.github/actions/download-sdk-runtime/action.yml new file mode 100644 index 0000000000..d17f6c5e56 --- /dev/null +++ b/.github/actions/download-sdk-runtime/action.yml @@ -0,0 +1,34 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +name: "Download SDK runtime" +description: "Download an SDK runtime artifact with at most three attempts; fail after exhaustion." +inputs: + artifact-name: + description: "Name of the target-specific runtime artifact." + required: true + path: + description: "Directory in which to download the artifact." + required: true +runs: + using: "composite" + steps: + - name: Download runtime artifact (attempt 1/3) + id: download-runtime + uses: actions/download-artifact@v8 + continue-on-error: true + with: + name: ${{ inputs.artifact-name }} + path: ${{ inputs.path }} + - name: Download runtime artifact (attempt 2/3) + id: download-runtime-retry-1 + if: steps.download-runtime.outcome == 'failure' + uses: actions/download-artifact@v8 + continue-on-error: true + with: + name: ${{ inputs.artifact-name }} + path: ${{ inputs.path }} + - name: Download runtime artifact (attempt 3/3) + if: steps.download-runtime.outcome == 'failure' && steps.download-runtime-retry-1.outcome == 'failure' + uses: actions/download-artifact@v8 + with: + name: ${{ inputs.artifact-name }} + path: ${{ inputs.path }} diff --git a/.github/actions/run-alpine-tests/action.yml b/.github/actions/run-alpine-tests/action.yml index 9dbb5e0ac4..67a7cf9767 100644 --- a/.github/actions/run-alpine-tests/action.yml +++ b/.github/actions/run-alpine-tests/action.yml @@ -1,5 +1,5 @@ # Runs an SDK command inside x64 Alpine and verifies the container is -# executing on musl with the requested transport. +# executing on musl. name: "Run Alpine SDK Tests" description: "Run an SDK test command in an x64 Alpine container." inputs: @@ -13,22 +13,18 @@ inputs: description: "SDK root inside the container." required: false default: "/workspace" - transport: - description: "SDK transport to test: default or inprocess." - required: true command: description: "Language-specific setup and test command." required: true runs: using: "composite" steps: - - name: Run ${{ inputs.transport }} tests in Alpine + - name: Run SDK tests in Alpine shell: bash env: ALPINE_TEST_COMMAND: ${{ inputs.command }} ALPINE_TEST_IMAGE: ${{ inputs.image }} ALPINE_TEST_SDK_ROOT: ${{ inputs.sdk-root }} - ALPINE_TEST_TRANSPORT: ${{ inputs.transport }} ALPINE_TEST_WORKDIR: ${{ inputs.workdir }} run: | script_path=$(mktemp "$RUNNER_TEMP/copilot-sdk-alpine-test.XXXXXX") @@ -43,11 +39,6 @@ runs: test "$(uname -m)" = "x86_64" ldd --version 2>&1 | grep -qi musl - case "$COPILOT_SDK_TEST_TRANSPORT" in - inprocess) export COPILOT_SDK_DEFAULT_CONNECTION=inprocess ;; - default) unset COPILOT_SDK_DEFAULT_CONNECTION ;; - *) echo "Unknown transport: $COPILOT_SDK_TEST_TRANSPORT" >&2; exit 1 ;; - esac SCRIPT printf '%s\n' "$ALPINE_TEST_COMMAND" >> "$script_path" chmod +x "$script_path" @@ -68,6 +59,5 @@ runs: --env RUST_BACKTRACE \ --env RUSTDOCFLAGS \ --env RUSTFLAGS \ - --env COPILOT_SDK_TEST_TRANSPORT="$ALPINE_TEST_TRANSPORT" \ "$ALPINE_TEST_IMAGE" \ /bin/sh /tmp/copilot-sdk-alpine-test.sh diff --git a/.github/skills/sdk-java-e2e-test/SKILL.md b/.github/skills/sdk-java-e2e-test/SKILL.md index 7ea8f277d9..1e074c027d 100644 --- a/.github/skills/sdk-java-e2e-test/SKILL.md +++ b/.github/skills/sdk-java-e2e-test/SKILL.md @@ -9,17 +9,12 @@ description: "Use this skill when creating a Java SDK surface-area E2E integrati These instructions work in both the runtime monorepo and the standalone public SDK repository. From the repository root, use `src/sdk` as the SDK root when -that directory exists; otherwise use the repository root itself. All paths and -commands below are relative to that SDK root, and command examples should be -run from it. See the companion `examples.md` for complete snapshot and test -examples. - -This skill covers the complete workflow for adding a new Java failsafe -integration test backed by a handcrafted YAML snapshot for the replay proxy. -Use it only when the new E2E tests Java SDK surface area; shared runtime -functionality available entirely through the SDK should instead be covered -in the TypeScript SDK suite (`nodejs/test/e2e/` under the SDK root, or -`src/sdk/nodejs/test/e2e/` from the runtime repository root). +that directory exists; otherwise use the repository root itself. All paths below +are relative to that SDK root. + +Use this skill only when the new E2E tests Java SDK surface area; shared +runtime functionality available entirely through the SDK belongs in the +TypeScript SDK suite (`nodejs/test/e2e/`). ## Overview @@ -27,43 +22,43 @@ The Java E2E tests use a **replay proxy** (`test/harness/replayingCapiProxy.ts`) that intercepts HTTP calls to the Copilot API and returns pre-recorded responses from YAML snapshot files. This avoids needing real authentication in CI. -**Key constraint:** Java's `CapiProxy.java` always sets `GITHUB_ACTIONS=true` -(line 104), which forces the replay proxy into read-only mode. You **cannot** -record snapshots by running Java tests — you must handcraft the YAML. +**Key constraint:** Java's `CapiProxy.start()` always sets `GITHUB_ACTIONS=true`, +which forces the replay proxy into read-only mode: a Java test cannot record a +snapshot, and every request must match an existing one. ## Step-by-Step Workflow ### Step 1: Choose a snapshot category and snapshot base name - Category = a directory under `test/snapshots/` (e.g., `system_message_sections`) -- Snapshot base name = the exact filename stem to use (already lowercase/underscore-separated), - e.g., `should_use_replaced_identity_section_in_response` +- Snapshot base name = the exact filename stem, already lowercase snake_case, + e.g., `should_use_replaced_identity_section_in_response`. Pass it verbatim to + `configureForTest`: it lowercases and replaces non-alphanumerics with `_` but + does not split camelCase, so `myTestMethod` would look for `mytestmethod.yaml`. - Resulting file: `test/snapshots//.yaml` -### Step 2: Create the YAML snapshot file - -The format is: - -```yaml -models: - - claude-sonnet-5 -conversations: - - messages: - - role: system - content: ${system} - - role: user - content: - - role: assistant - content: -``` - -**Rules:** -- `${system}` is a placeholder that matches ANY system message content -- `${workdir}` in tool arguments is substituted with the actual temp workDir -- Each conversation entry represents one request-response exchange -- For multi-turn, add multiple conversation entries -- For tool calls, include `tool_calls` on assistant messages and `role: tool` for results -- The user content must **exactly match** what your test sends (after normalization) +### Step 2: Use a recorded snapshot + +The SDK guide forbids hand-authored model responses (`CONTRIBUTING.md`, +"Recording and replaying SDK tests"). Reuse a capture that another SDK suite +recorded for the same conversation, as most Java ITs do; if none exists, record +one through the TypeScript SDK suite with the same snapshot name and prompt, +following that guide. + +**Replay rules:** +- Every request's system message is replaced with `${system}` before matching, + so system content is never compared, and an assertion on the reply cannot + prove that a system-message setting reached the model. +- User content must **exactly match** the snapshot's (after normalization), so + copy the prompt from the YAML. +- `${workdir}` stands for the test's temp workDir in tool arguments and results. +- A request matches when its messages equal a conversation up to an assistant + message; the proxy replies with that assistant message and any assistant + messages directly after it. +- A request that matches nothing fails. The proxy's stderr, echoed as + `[CapiProxy stderr]`, reports "No cached response found" with the first + mismatching message per conversation, and `configureForTest` logs the prompts + the snapshot expects. ### Step 3: Create the Java IT test class @@ -146,9 +141,11 @@ pnpm run generate:sdk:java pnpm run build:cli ``` -The facade's runtime paths apply only to its child process. For a focused Maven -run, set the same-checkout wrapper path explicitly, then use the `verify` -lifecycle so test-resource setup and `failsafe:verify` both run: +`pnpm run test:sdk:java` runs the whole suite and takes no test filter. For a +focused Maven run, pass the same-checkout wrapper explicitly (without +`copilot.cli.path` the tests use the release pinned by `nodejs/package.json`), +then use the `verify` lifecycle so test-resource setup and `failsafe:verify` +both run: ```sh cd src/sdk/java @@ -157,7 +154,9 @@ COPILOT_CLI_PATH=/dist-cli/prebuilds//copilot-runtime ./mvnw -pl sdk verify -Dit.test="MyFeatureIT#myTestMethod" -Dcopilot.cli.path="$COPILOT_CLI_PATH" ``` -Use `copilot-runtime.exe` for the wrapper name on Windows. +`` is Node's `-` (for example +`linux-x64`, `darwin-arm64`, `win32-x64`); use `copilot-runtime.exe` for the +wrapper name on Windows. In the standalone SDK repository, start at `java/` and use the pinned runtime: @@ -175,77 +174,15 @@ In the standalone SDK repository, start at `java/` and use the pinned runtime: | Replay proxy (TypeScript) | `test/harness/replayingCapiProxy.ts` | | Proxy server entry point | `test/harness/server.ts` | | Snapshot files | `test/snapshots//.yaml` | -| Existing IT tests for reference | `java/sdk/src/test/java/com/github/copilot/*IT.java` | - -## How the Proxy Matches Requests - -1. The proxy normalizes the incoming request's messages -2. It compares against each conversation in the YAML: - - System message matches if YAML has `${system}` (wildcard) - - User messages are compared by content (exact text match) - - Tool results are compared after normalizing `${workdir}` paths -3. If a match is found, the proxy returns the **next assistant message after the matched request prefix** -4. If no match, in CI mode (`GITHUB_ACTIONS=true`) it errors with "No cached response found" - -## YAML Format for Tool Calls - -If your test involves tool use: - -```yaml -conversations: - # First exchange: model wants to call a tool - - messages: - - role: system - content: ${system} - - role: user - content: Read the file test.txt - - role: assistant - content: I'll read that file. - tool_calls: - - id: toolcall_0 - type: function - function: - name: view - arguments: '{"path":"${workdir}/test.txt"}' - # Second exchange: after tool result is provided, model gives final answer - - messages: - - role: system - content: ${system} - - role: user - content: Read the file test.txt - - role: assistant - content: I'll read that file. - tool_calls: - - id: toolcall_0 - type: function - function: - name: view - arguments: '{"path":"${workdir}/test.txt"}' - - role: tool - tool_call_id: toolcall_0 - content: "1. Hello world!" - - role: assistant - content: The file test.txt contains "Hello world!" -``` +| Existing IT tests for reference | `java/sdk/src/test/java/com/github/copilot/**/*IT.java`; `SystemMessageSectionsIT` reuses Node.js suite captures, including a `view` tool call | + +## Tests that call built-in tools -**Important:** When the model calls tools like `view`, the CLI actually executes -them locally. The file must exist in the test's workDir. Create it in your test -before sending the prompt: +When the snapshot has the model call a built-in tool such as `view`, the CLI +really executes it in the test's workDir, and the next request carries its +result, which must match the snapshot's. Create any file the tool reads before +sending the prompt, with exactly the content the snapshot's tool result shows: ```java -Files.writeString(ctx.getWorkDir().resolve("test.txt"), "Hello world!\n"); +Files.writeString(ctx.getWorkDir().resolve("test.txt"), "Hello transform!"); ``` - -## Common Pitfalls - -1. **Prompt mismatch** — The user content in YAML must exactly match what - `session.sendAndWait(new MessageOptions().setPrompt("..."))` sends. -2. **Forgetting `${system}`** — Always use `${system}` for the system role content - unless testing a specific system message matching scenario. -3. **Tool execution** — If the snapshot has the model calling `view` or other - built-in tools, the CLI will actually execute those tools. Files must exist. -4. **Snapshot name parameter** — pass the explicit snapshot base name to - `configureForTest`, e.g., `configureForTest("category", "my_method_name")`. - Do not rely on camelCase-to-snake_case conversion. -5. **Cannot record via Java** — `CapiProxy.java` forces `GITHUB_ACTIONS=true`. - Always handcraft snapshots or use the Node.js proxy directly for recording. diff --git a/.github/skills/sdk-java-e2e-test/examples.md b/.github/skills/sdk-java-e2e-test/examples.md deleted file mode 100644 index 6cd98d1587..0000000000 --- a/.github/skills/sdk-java-e2e-test/examples.md +++ /dev/null @@ -1,181 +0,0 @@ -# Examples: New Java E2E Test with YAML Snapshot - -These examples document existing Java tests. For new E2E tests of shared -runtime behavior available through the SDK, use the TypeScript SDK suite -instead; add Java E2Es only for Java SDK surface area. - -## Example 1: Simple single-turn conversation (no tool calls) - -### Snapshot YAML - -File: `test/snapshots/system_message_sections/should_use_replaced_identity_section_in_response.yaml` - -```yaml -models: - - claude-sonnet-5 -conversations: - - messages: - - role: system - content: ${system} - - role: user - content: Who are you? - - role: assistant - content: >- - I'm Botanica, your helpful gardening assistant! I'm here to help you - with all things related to plants and gardening. Whether you have - questions about plant care, garden design, soil preparation, pest - management, or anything else in the world of gardening, I'm happy to - help. What would you like to know about plants or gardening today? -``` - -### Corresponding Java test method - -```java -@Test -void shouldUseReplacedIdentitySectionInResponse() throws Exception { - ctx.configureForTest("system_message_sections", "should_use_replaced_identity_section_in_response"); - - var systemMessage = new SystemMessageConfig().setMode(SystemMessageMode.CUSTOMIZE) - .setSections(Map.of(SystemMessageSections.IDENTITY, - new SectionOverride().setAction(SectionOverrideAction.REPLACE) - .setContent("You are a helpful gardening assistant called Botanica. " - + "You only answer questions about plants and gardening."))); - - try (CopilotClient client = ctx.createClient()) { - CopilotSession session = client.createSession(new SessionConfig().setSystemMessage(systemMessage) - .setOnPermissionRequest(PermissionHandler.APPROVE_ALL)).get(30, TimeUnit.SECONDS); - - try { - AssistantMessageEvent response = session - .sendAndWait(new MessageOptions().setPrompt("Who are you?"), 60_000).get(90, TimeUnit.SECONDS); - - assertNotNull(response, "Expected a response from the assistant"); - String content = response.getData().content().toLowerCase(); - assertTrue(content.contains("botanica") || content.contains("garden") || content.contains("plant"), - "Expected response to reflect the replaced identity section, but got: " - + response.getData().content()); - } finally { - session.close(); - } - } -} -``` - -**Key points:** -- `configureForTest("system_message_sections", "should_use_replaced_identity_section_in_response")` - maps to `test/snapshots/system_message_sections/should_use_replaced_identity_section_in_response.yaml` -- The prompt `"Who are you?"` exactly matches the YAML's user content -- `ctx.createClient()` uses `fake-token-for-e2e-tests` — works in CI - ---- - -## Example 2: Multi-turn with tool calls (from existing tests) - -### Snapshot YAML - -File: `test/snapshots/system_message_transform/should_invoke_transform_callbacks_with_section_content.yaml` - -```yaml -models: - - claude-sonnet-5 -conversations: - # First exchange: model decides to call tools - - messages: - - role: system - content: ${system} - - role: user - content: Read the contents of test.txt and tell me what it says - - role: assistant - content: I'll read the test.txt file for you. - tool_calls: - - id: toolcall_0 - type: function - function: - name: report_intent - arguments: '{"intent":"Reading test.txt file"}' - - id: toolcall_1 - type: function - function: - name: view - arguments: '{"path":"${workdir}/test.txt"}' - # Second exchange: after tool results come back, model gives final answer - - messages: - - role: system - content: ${system} - - role: user - content: Read the contents of test.txt and tell me what it says - - role: assistant - content: I'll read the test.txt file for you. - tool_calls: - - id: toolcall_0 - type: function - function: - name: report_intent - arguments: '{"intent":"Reading test.txt file"}' - - id: toolcall_1 - type: function - function: - name: view - arguments: '{"path":"${workdir}/test.txt"}' - - role: tool - tool_call_id: toolcall_0 - content: Intent logged - - role: tool - tool_call_id: toolcall_1 - content: 1. Hello transform! - - role: assistant - content: |- - The file test.txt contains: - ``` - Hello transform! - ``` -``` - -### Corresponding Java test method - -```java -@Test -void transformOnIdentitySectionReceivesNonEmptyContent() throws Exception { - ctx.configureForTest("system_message_transform", "should_invoke_transform_callbacks_with_section_content"); - - ConcurrentHashMap capturedContent = new ConcurrentHashMap<>(); - - var systemMessage = new SystemMessageConfig().setMode(SystemMessageMode.CUSTOMIZE) - .setSections(Map.of(SystemMessageSections.IDENTITY, new SectionOverride().setTransform(content -> { - capturedContent.put("identity", content); - return CompletableFuture.completedFuture(content); - }), SystemMessageSections.TONE, new SectionOverride().setTransform(content -> { - capturedContent.put("tone", content); - return CompletableFuture.completedFuture(content); - }))); - - try (CopilotClient client = ctx.createClient()) { - // Create the file the snapshot expects the CLI view tool to read - Path testFile = ctx.getWorkDir().resolve("test.txt"); - Files.writeString(testFile, "Hello transform!"); - - CopilotSession session = client.createSession(new SessionConfig().setSystemMessage(systemMessage) - .setOnPermissionRequest(PermissionHandler.APPROVE_ALL)).get(30, TimeUnit.SECONDS); - - try { - AssistantMessageEvent response = session - .sendAndWait(new MessageOptions() - .setPrompt("Read the contents of test.txt and tell me what it says"), 60_000) - .get(90, TimeUnit.SECONDS); - - assertNotNull(response, "Expected a response from the assistant"); - - String identityContent = capturedContent.get("identity"); - assertNotNull(identityContent, "Expected identity transform callback to be invoked"); - assertTrue(!identityContent.isBlank(), "Expected identity section content to be non-empty"); - } finally { - session.close(); - } - } -} -``` - -**Key points:** -- The file `test.txt` must be created in `ctx.getWorkDir()` **before** sending the prompt -- The CLI's `view` tool will actually read that file; the YAML's tool result `"1. Hello transform!"` must match what `view` returns for that file content -- Two conversation entries: first for the tool-call decision, second for the final response after tool results diff --git a/.github/workflows/sdk-dotnet.yml b/.github/workflows/sdk-dotnet.yml index 4514817827..4e9671c9a9 100644 --- a/.github/workflows/sdk-dotnet.yml +++ b/.github/workflows/sdk-dotnet.yml @@ -1,5 +1,5 @@ # Invoked by sdk.yml to run the .NET SDK build/test matrix, documentation, -# backend coverage, full subprocess CAPI coverage, and focused in-process smoke +# full subprocess CAPI coverage, and focused in-process smoke # coverage on Linux and x64 musl. name: "SDK .NET" @@ -25,13 +25,9 @@ permissions: jobs: dotnet-linux-x64: - name: ".NET (ubuntu-latest, ${{ matrix.transport }}, CAPI)" + name: ".NET (ubuntu-latest, CAPI)" if: inputs.platform == 'all' || inputs.platform == 'linux-x64' timeout-minutes: 30 - strategy: - fail-fast: false - matrix: - transport: [default, inprocess] runs-on: ubuntu-latest defaults: run: @@ -39,16 +35,15 @@ jobs: working-directory: ${{ inputs.sdk-home }}/dotnet env: COPILOT_SDK_E2E_BACKEND: capi - DOTNET_TEST_FILTER: ${{ matrix.transport == 'inprocess' && 'FullyQualifiedName~GitHub.Copilot.Test.E2E.ClientE2ETests.Should_Start_And_Connect_Over_InProcess_Ffi|FullyQualifiedName~GitHub.Copilot.Test.E2E.SessionE2ETests.Should_Receive_Session_Events|FullyQualifiedName~GitHub.Copilot.Test.E2E.ClientOptionsE2ETests.Should_Use_Configured_GitHub_Host_For_Authentication' || '' }} - COPILOT_SDK_DEFAULT_CONNECTION: ${{ matrix.transport == 'inprocess' && 'inprocess' || '' }} + DOTNET_TEST_RESULTS_DIRECTORY: TestResults/subprocess steps: - uses: actions/checkout@v7 timeout-minutes: 4 with: persist-credentials: false - - uses: actions/download-artifact@v8 + - uses: ./.github/actions/download-sdk-runtime with: - name: executable-sdk-Linux-X64-gnu + artifact-name: executable-sdk-Linux-X64-gnu path: ${{ github.workspace }} - run: node "$SDK_HOME/scripts/ci/runtime-artifact.mjs" restore && node "$SDK_HOME/scripts/ci/runtime-artifact.mjs" prepare working-directory: . @@ -61,11 +56,10 @@ jobs: - run: dotnet restore -p:CopilotCliBinaryPath="$COPILOT_RUNTIME_BINARY_PATH" -p:CopilotSkipCliDownload=true # TEMPORARY MERGE QUEUE REDUCTION: pull requests and main pushes retain # static checks; merge groups rerun only SDK compatibility coverage. - - if: github.event_name != 'merge_group' && matrix.transport == 'default' + - if: github.event_name != 'merge_group' run: dotnet format --verify-no-changes --no-restore - run: dotnet build --no-restore -p:CopilotCliBinaryPath="$COPILOT_RUNTIME_BINARY_PATH" -p:CopilotSkipCliDownload=true - name: Test structured output with reflection enabled - if: matrix.transport == 'default' env: DOTNET_ROLL_FORWARD: Major run: >- @@ -76,13 +70,23 @@ jobs: --filter "FullyQualifiedName~GitHub.Copilot.Test.Unit.ClientSessionLifetimeTests.StructuredOutput" - run: npm ci --ignore-scripts working-directory: ${{ inputs.sdk-home }}/test/harness - - env: + - name: Test out-of-process + id: subprocess + env: + COPILOT_HMAC_KEY: ${{ secrets.COPILOT_DEVELOPER_CLI_INTEGRATION_HMAC_KEY }} + run: ../scripts/ci/run-dotnet-tests.sh + - name: Test in-process smoke + if: ${{ !cancelled() && (steps.subprocess.outcome == 'success' || steps.subprocess.outcome == 'failure') }} + env: COPILOT_HMAC_KEY: ${{ secrets.COPILOT_DEVELOPER_CLI_INTEGRATION_HMAC_KEY }} + COPILOT_SDK_DEFAULT_CONNECTION: inprocess + DOTNET_TEST_RESULTS_DIRECTORY: TestResults/inprocess + DOTNET_TEST_FILTER: "FullyQualifiedName~GitHub.Copilot.Test.E2E.ClientE2ETests.Should_Start_And_Connect_Over_InProcess_Ffi|FullyQualifiedName~GitHub.Copilot.Test.E2E.SessionE2ETests.Should_Receive_Session_Events|FullyQualifiedName~GitHub.Copilot.Test.E2E.ClientOptionsE2ETests.Should_Use_Configured_GitHub_Host_For_Authentication" run: ../scripts/ci/run-dotnet-tests.sh - if: failure() uses: actions/upload-artifact@v7 with: - name: dotnet-test-diagnostics-ubuntu-latest-${{ matrix.transport }}-capi-${{ github.run_attempt }} + name: dotnet-test-diagnostics-ubuntu-latest-capi-${{ github.run_attempt }} path: ${{ inputs.sdk-home }}/dotnet/TestResults/ if-no-files-found: warn retention-days: 7 @@ -103,9 +107,9 @@ jobs: timeout-minutes: 4 with: persist-credentials: false - - uses: actions/download-artifact@v8 + - uses: ./.github/actions/download-sdk-runtime with: - name: executable-sdk-macOS-ARM64 + artifact-name: executable-sdk-macOS-ARM64 path: ${{ github.workspace }} - run: node "$SDK_HOME/scripts/ci/runtime-artifact.mjs" restore && node "$SDK_HOME/scripts/ci/runtime-artifact.mjs" prepare working-directory: . @@ -147,9 +151,9 @@ jobs: timeout-minutes: 4 with: persist-credentials: false - - uses: actions/download-artifact@v8 + - uses: ./.github/actions/download-sdk-runtime with: - name: executable-sdk-Windows-X64 + artifact-name: executable-sdk-Windows-X64 path: ${{ github.workspace }} - run: node "$SDK_HOME/scripts/ci/runtime-artifact.mjs" restore && node "$SDK_HOME/scripts/ci/runtime-artifact.mjs" prepare working-directory: . @@ -175,75 +179,21 @@ jobs: if-no-files-found: warn retention-days: 7 - # Exercise non-CAPI model backends through .NET only instead of multiplying - # them across every language and execution mode; all SDKs cover both modes with CAPI. - dotnet-backends: - name: ".NET (ubuntu-latest, default, ${{ matrix.backend }})" - if: inputs.platform == 'all' || inputs.platform == 'linux-x64' - runs-on: ubuntu-latest - timeout-minutes: 30 - strategy: - fail-fast: false - matrix: - backend: [anthropic-messages, openai-responses, openai-completions] - defaults: - run: - shell: bash - working-directory: ${{ inputs.sdk-home }}/dotnet - env: - COPILOT_SDK_E2E_BACKEND: ${{ matrix.backend }} - DOTNET_TEST_FILTER: "FullyQualifiedName~GitHub.Copilot.Test.E2E&E2EBackend!=SelfConfiguredBackend&E2EBackend!=CapiOnly" - steps: - - uses: actions/checkout@v7 - timeout-minutes: 4 - with: - persist-credentials: false - - uses: actions/download-artifact@v8 - with: - name: executable-sdk-Linux-X64-gnu - path: ${{ github.workspace }} - - run: node "$SDK_HOME/scripts/ci/runtime-artifact.mjs" restore && node "$SDK_HOME/scripts/ci/runtime-artifact.mjs" prepare - working-directory: . - - uses: actions/setup-dotnet@v6 - with: - dotnet-version: "10.0.x" - - uses: actions/setup-node@v6 - with: - node-version: 22 - - run: dotnet restore -p:CopilotCliBinaryPath="$COPILOT_RUNTIME_BINARY_PATH" -p:CopilotSkipCliDownload=true - - run: dotnet build --no-restore -p:CopilotCliBinaryPath="$COPILOT_RUNTIME_BINARY_PATH" -p:CopilotSkipCliDownload=true - - run: npm ci --ignore-scripts - working-directory: ${{ inputs.sdk-home }}/test/harness - - env: - COPILOT_HMAC_KEY: ${{ secrets.COPILOT_DEVELOPER_CLI_INTEGRATION_HMAC_KEY }} - run: ../scripts/ci/run-dotnet-tests.sh - - if: failure() - uses: actions/upload-artifact@v7 - with: - name: dotnet-test-diagnostics-ubuntu-latest-default-${{ matrix.backend }}-${{ github.run_attempt }} - path: ${{ inputs.sdk-home }}/dotnet/TestResults/ - if-no-files-found: warn - retention-days: 7 - dotnet-musl-x64: - name: ".NET (Alpine x64, ${{ matrix.transport }}, CAPI)" + name: ".NET (Alpine x64, CAPI)" if: inputs.platform == 'all' || inputs.platform == 'linuxmusl-x64' runs-on: ubuntu-latest timeout-minutes: 20 env: COPILOT_SDK_E2E_BACKEND: capi - strategy: - fail-fast: false - matrix: - transport: [default, inprocess] steps: - uses: actions/checkout@v7 timeout-minutes: 4 with: persist-credentials: false - - uses: actions/download-artifact@v8 + - uses: ./.github/actions/download-sdk-runtime with: - name: executable-sdk-Linux-X64-musl + artifact-name: executable-sdk-Linux-X64-musl path: ${{ github.workspace }} - run: node "$SDK_HOME/scripts/ci/runtime-artifact.mjs" restore working-directory: . @@ -255,7 +205,6 @@ jobs: image: mcr.microsoft.com/dotnet/sdk:10.0-alpine sdk-root: /workspace/${{ inputs.sdk-home }} workdir: /workspace/${{ inputs.sdk-home }} - transport: ${{ matrix.transport }} command: | dotnet --info | grep -Eq "RID:[[:space:]]+linux-musl-x64" apk add --no-cache nodejs npm @@ -278,28 +227,33 @@ jobs: dotnet restore "$COPILOT_SDK_ROOT/dotnet/test/GitHub.Copilot.SDK.Test.csproj" \ -p:CopilotCliBinaryPath="$COPILOT_RUNTIME_BINARY_PATH" \ -p:CopilotSkipCliDownload=true - set -- - if [ "$COPILOT_SDK_TEST_TRANSPORT" = "inprocess" ]; then - set -- --filter 'FullyQualifiedName~GitHub.Copilot.Test.E2E.ClientE2ETests.Should_Start_And_Connect_Over_InProcess_Ffi|FullyQualifiedName~GitHub.Copilot.Test.E2E.SessionE2ETests.Should_Receive_Session_Events|FullyQualifiedName~GitHub.Copilot.Test.E2E.ClientOptionsE2ETests.Should_Use_Configured_GitHub_Host_For_Authentication' - fi - dotnet test "$COPILOT_SDK_ROOT/dotnet/test/GitHub.Copilot.SDK.Test.csproj" \ - --no-restore \ + dotnet build "$COPILOT_SDK_ROOT/dotnet/test/GitHub.Copilot.SDK.Test.csproj" \ + --no-restore --framework net8.0 \ + -p:CopilotCliBinaryPath="$COPILOT_RUNTIME_BINARY_PATH" \ + -p:CopilotSkipCliDownload=true \ + -p:RunAnalyzers=false + set -- "$COPILOT_SDK_ROOT/dotnet/test/GitHub.Copilot.SDK.Test.csproj" \ + --no-build \ --framework net8.0 \ -v n \ --blame-hang \ --blame-hang-timeout 10m \ --blame-hang-dump-type none \ --logger "trx;LogFilePrefix=test-results" \ - --results-directory "$COPILOT_SDK_ROOT/dotnet/TestResults" \ - "$@" \ -p:CopilotCliBinaryPath="$COPILOT_RUNTIME_BINARY_PATH" \ -p:CopilotSkipCliDownload=true \ -p:RunAnalyzers=false + status=0 + dotnet test "$@" --results-directory "$COPILOT_SDK_ROOT/dotnet/TestResults/subprocess" || status=$? + COPILOT_SDK_DEFAULT_CONNECTION=inprocess dotnet test "$@" \ + --results-directory "$COPILOT_SDK_ROOT/dotnet/TestResults/inprocess" \ + --filter 'FullyQualifiedName~GitHub.Copilot.Test.E2E.ClientE2ETests.Should_Start_And_Connect_Over_InProcess_Ffi|FullyQualifiedName~GitHub.Copilot.Test.E2E.SessionE2ETests.Should_Receive_Session_Events|FullyQualifiedName~GitHub.Copilot.Test.E2E.ClientOptionsE2ETests.Should_Use_Configured_GitHub_Host_For_Authentication' || status=$? + exit "$status" - name: Upload .NET test diagnostics if: failure() uses: actions/upload-artifact@v7 with: - name: dotnet-test-diagnostics-alpine-x64-${{ matrix.transport }}-capi-${{ github.run_attempt }} + name: dotnet-test-diagnostics-alpine-x64-capi-${{ github.run_attempt }} path: ${{ inputs.sdk-home }}/dotnet/TestResults/ if-no-files-found: warn retention-days: 7 diff --git a/.github/workflows/sdk-go.yml b/.github/workflows/sdk-go.yml index f613027d39..24ef16a35b 100644 --- a/.github/workflows/sdk-go.yml +++ b/.github/workflows/sdk-go.yml @@ -25,13 +25,12 @@ permissions: jobs: go: - name: "Go (${{ matrix.os }}, ${{ matrix.transport }})" + name: "Go (${{ matrix.os }})" if: contains(fromJSON('["all","linux-x64","darwin-arm64","win32-x64"]'), inputs.platform) strategy: fail-fast: false matrix: os: ${{ fromJSON(inputs.platform == 'linux-x64' && '["ubuntu-latest"]' || inputs.platform == 'darwin-arm64' && '["macos-latest"]' || inputs.platform == 'win32-x64' && '["windows-latest"]' || '["ubuntu-latest","macos-latest","windows-latest"]') }} - transport: [default, inprocess] runs-on: ${{ matrix.os }} defaults: run: @@ -42,9 +41,9 @@ jobs: timeout-minutes: 4 with: persist-credentials: false - - uses: actions/download-artifact@v8 + - uses: ./.github/actions/download-sdk-runtime with: - name: ${{ runner.os == 'Linux' && format('executable-sdk-{0}-{1}-gnu', runner.os, runner.arch) || format('executable-sdk-{0}-{1}', runner.os, runner.arch) }} + artifact-name: ${{ runner.os == 'Linux' && format('executable-sdk-{0}-{1}-gnu', runner.os, runner.arch) || format('executable-sdk-{0}-{1}', runner.os, runner.arch) }} path: ${{ github.workspace }} - run: node "$SDK_HOME/scripts/ci/runtime-artifact.mjs" restore && node "$SDK_HOME/scripts/ci/runtime-artifact.mjs" prepare working-directory: . @@ -56,52 +55,51 @@ jobs: ${{ inputs.sdk-home }}/go/samples/go.sum # TEMPORARY MERGE QUEUE REDUCTION: pull requests and main pushes retain # static checks; merge groups rerun only SDK compatibility coverage. - - if: github.event_name != 'merge_group' && runner.os == 'Linux' && matrix.transport == 'default' + - if: github.event_name != 'merge_group' && runner.os == 'Linux' run: | go fmt ./... git diff --exit-code - - if: github.event_name != 'merge_group' && runner.os == 'Linux' && matrix.transport == 'default' + - if: github.event_name != 'merge_group' && runner.os == 'Linux' uses: golangci/golangci-lint-action@v9 with: working-directory: ${{ inputs.sdk-home }}/go version: latest args: --timeout=5m - - if: runner.os == 'Linux' && matrix.transport == 'default' + - if: runner.os == 'Linux' run: go test ./... working-directory: ${{ inputs.sdk-home }}/go/samples - run: npm ci --ignore-scripts working-directory: ${{ inputs.sdk-home }}/test/harness - if: runner.os == 'Windows' run: pwsh.exe -Command "Write-Host 'PowerShell ready'" - - env: + - name: Test out-of-process + id: subprocess + env: + COPILOT_HMAC_KEY: ${{ secrets.COPILOT_DEVELOPER_CLI_INTEGRATION_HMAC_KEY }} + run: /bin/bash test.sh + - name: Test in-process smoke + if: ${{ !cancelled() && (steps.subprocess.outcome == 'success' || steps.subprocess.outcome == 'failure') }} + env: COPILOT_HMAC_KEY: ${{ secrets.COPILOT_DEVELOPER_CLI_INTEGRATION_HMAC_KEY }} - COPILOT_SDK_DEFAULT_CONNECTION: ${{ matrix.transport == 'inprocess' && 'inprocess' || '' }} - GOFLAGS: ${{ matrix.transport == 'inprocess' && '-tags=copilot_inprocess' || '' }} + COPILOT_SDK_DEFAULT_CONNECTION: inprocess + GOFLAGS: -tags=copilot_inprocess run: | - if [ "$COPILOT_SDK_DEFAULT_CONNECTION" = "inprocess" ]; then - go test -v -race -timeout=20m ./internal/e2e -run '^TestInProcessFfiE2E$' - go test -v -race -timeout=20m ./internal/e2e -run '^TestClientE2E$/^should_use_configured_github_host_for_authentication$' - else - /bin/bash test.sh - fi + go test -v -race -timeout=20m ./internal/e2e -run '^TestInProcessFfiE2E$' + go test -v -race -timeout=20m ./internal/e2e -run '^TestClientE2E$/^should_use_configured_github_host_for_authentication$' go-musl-x64: - name: "Go (Alpine x64, ${{ matrix.transport }})" + name: "Go (Alpine x64)" if: inputs.platform == 'all' || inputs.platform == 'linuxmusl-x64' runs-on: ubuntu-latest timeout-minutes: 20 - strategy: - fail-fast: false - matrix: - transport: [default, inprocess] steps: - uses: actions/checkout@v7 timeout-minutes: 4 with: persist-credentials: false - - uses: actions/download-artifact@v8 + - uses: ./.github/actions/download-sdk-runtime with: - name: executable-sdk-Linux-X64-musl + artifact-name: executable-sdk-Linux-X64-musl path: ${{ github.workspace }} - run: node "$SDK_HOME/scripts/ci/runtime-artifact.mjs" restore working-directory: . @@ -112,7 +110,6 @@ jobs: image: golang:1.24-alpine sdk-root: /workspace/${{ inputs.sdk-home }} workdir: /workspace/${{ inputs.sdk-home }} - transport: ${{ matrix.transport }} command: | apk add --no-cache build-base nodejs npm npm --prefix "$COPILOT_SDK_ROOT/nodejs" ci --ignore-scripts @@ -130,21 +127,17 @@ jobs: *) echo "Expected the linuxmusl-x64 runtime, got: $COPILOT_CLI_PATH" >&2; exit 1 ;; esac - if [ "$COPILOT_SDK_TEST_TRANSPORT" = "inprocess" ]; then - export GOFLAGS="-tags=copilot_inprocess" - else - unset GOFLAGS - fi export CGO_ENABLED=1 test "$(go env GOARCH)" = "amd64" test "$(go env CGO_ENABLED)" = "1" cd "$COPILOT_SDK_ROOT/go" - if [ "$COPILOT_SDK_TEST_TRANSPORT" = "inprocess" ]; then - go test -v -race -timeout=20m ./internal/e2e -run '^TestInProcessFfiE2E$' - go test -v -race -timeout=20m ./internal/e2e -run '^TestClientE2E$/^should_use_configured_github_host_for_authentication$' - else - /bin/bash test.sh - fi + status=0 + /bin/bash test.sh || status=$? + export COPILOT_SDK_DEFAULT_CONNECTION=inprocess + export GOFLAGS="-tags=copilot_inprocess" + go test -v -race -timeout=20m ./internal/e2e -run '^TestInProcessFfiE2E$' || status=$? + go test -v -race -timeout=20m ./internal/e2e -run '^TestClientE2E$/^should_use_configured_github_host_for_authentication$' || status=$? + exit "$status" docs-go: name: "Docs (Go)" diff --git a/.github/workflows/sdk-java.yml b/.github/workflows/sdk-java.yml index a69caa71d3..408298d88c 100644 --- a/.github/workflows/sdk-java.yml +++ b/.github/workflows/sdk-java.yml @@ -40,9 +40,9 @@ jobs: timeout-minutes: 4 with: persist-credentials: false - - uses: actions/download-artifact@v8 + - uses: ./.github/actions/download-sdk-runtime with: - name: executable-sdk-Linux-X64-gnu + artifact-name: executable-sdk-Linux-X64-gnu path: ${{ github.workspace }} - run: node "$SDK_HOME/scripts/ci/runtime-artifact.mjs" restore && node "$SDK_HOME/scripts/ci/runtime-artifact.mjs" prepare working-directory: . @@ -64,6 +64,7 @@ jobs: - if: github.event_name != 'merge_group' && matrix.test-jdk == '25' run: ./mvnw spotless:check - if: matrix.test-jdk == '25' + id: subprocess env: CI: "true" run: ./mvnw -pl sdk verify -Dskip.test.harness=true -Dcopilot.cli.path="$COPILOT_CLI_PATH" @@ -76,6 +77,23 @@ jobs: env: CI: "true" run: ./mvnw -pl sdk jacoco:prepare-agent@wire-up-coverage-instrumentation antrun:run@print-test-jdk-banner surefire:test failsafe:integration-test failsafe:verify jacoco:report@build-coverage-report-from-tests -Denforcer.skip=true -Dcopilot.cli.path="$COPILOT_CLI_PATH" + - name: Test in-process smoke + if: ${{ !cancelled() && matrix.test-jdk == '25' && (steps.subprocess.outcome == 'success' || steps.subprocess.outcome == 'failure') }} + env: + CI: "true" + run: | + mkdir -p sdk/target/subprocess-reports + for reports in surefire-reports surefire-reports-isolated failsafe-reports; do + if [ -d "sdk/target/$reports" ]; then + mv "sdk/target/$reports" sdk/target/subprocess-reports/ + fi + done + ./mvnw verify -Pinprocess \ + -Dskip.test.harness=true \ + -Dtest=NoTestsForInProcessSmoke \ + -Dsurefire.failIfNoSpecifiedTests=false \ + -Dit.test=InProcessTransportIT,AuthHostE2ETest \ + -Dcopilot.inprocess.cli.path="$COPILOT_CLI_PATH" - if: failure() uses: actions/upload-artifact@v7 with: @@ -84,15 +102,16 @@ jobs: ${{ inputs.sdk-home }}/java/sdk/target/surefire-reports/ ${{ inputs.sdk-home }}/java/sdk/target/surefire-reports-isolated/ ${{ inputs.sdk-home }}/java/sdk/target/failsafe-reports/ + ${{ inputs.sdk-home }}/java/sdk/target/subprocess-reports/ retention-days: 7 java-inprocess: name: "Java (inprocess, ${{ matrix.classifier }})" - if: contains(fromJSON('["all","linux-x64","darwin-arm64","win32-x64"]'), inputs.platform) + if: contains(fromJSON('["all","darwin-arm64","win32-x64"]'), inputs.platform) strategy: fail-fast: false matrix: - include: ${{ fromJSON(inputs.platform == 'linux-x64' && '[{"os":"ubuntu-latest","classifier":"linux-x64"}]' || inputs.platform == 'darwin-arm64' && '[{"os":"macos-26","classifier":"darwin-arm64"}]' || inputs.platform == 'win32-x64' && '[{"os":"windows-latest","classifier":"win32-x64"}]' || '[{"os":"ubuntu-latest","classifier":"linux-x64"},{"os":"windows-latest","classifier":"win32-x64"},{"os":"macos-26","classifier":"darwin-arm64"}]') }} + include: ${{ fromJSON(inputs.platform == 'darwin-arm64' && '[{"os":"macos-26","classifier":"darwin-arm64"}]' || inputs.platform == 'win32-x64' && '[{"os":"windows-latest","classifier":"win32-x64"}]' || '[{"os":"windows-latest","classifier":"win32-x64"},{"os":"macos-26","classifier":"darwin-arm64"}]') }} runs-on: ${{ matrix.os }} defaults: run: @@ -103,9 +122,9 @@ jobs: timeout-minutes: 4 with: persist-credentials: false - - uses: actions/download-artifact@v8 + - uses: ./.github/actions/download-sdk-runtime with: - name: ${{ runner.os == 'Linux' && format('executable-sdk-{0}-{1}-gnu', runner.os, runner.arch) || format('executable-sdk-{0}-{1}', runner.os, runner.arch) }} + artifact-name: ${{ runner.os == 'Linux' && format('executable-sdk-{0}-{1}-gnu', runner.os, runner.arch) || format('executable-sdk-{0}-{1}', runner.os, runner.arch) }} path: ${{ github.workspace }} - run: node "$SDK_HOME/scripts/ci/runtime-artifact.mjs" restore && node "$SDK_HOME/scripts/ci/runtime-artifact.mjs" prepare working-directory: . @@ -119,14 +138,12 @@ jobs: node-version: 22 - env: CI: "true" - MAVEN_ARGS: ${{ matrix.maven-args }} run: >- ./mvnw clean verify -Pinprocess -Dtest=NoTestsForInProcessSmoke -Dsurefire.failIfNoSpecifiedTests=false -Dit.test=InProcessTransportIT,AuthHostE2ETest -Dcopilot.inprocess.cli.path="$COPILOT_CLI_PATH" - $MAVEN_ARGS - if: failure() uses: actions/upload-artifact@v7 with: @@ -147,9 +164,9 @@ jobs: timeout-minutes: 4 with: persist-credentials: false - - uses: actions/download-artifact@v8 + - uses: ./.github/actions/download-sdk-runtime with: - name: executable-sdk-Linux-X64-musl + artifact-name: executable-sdk-Linux-X64-musl path: ${{ github.workspace }} - run: node "$SDK_HOME/scripts/ci/runtime-artifact.mjs" restore working-directory: . diff --git a/.github/workflows/sdk-nodejs.yml b/.github/workflows/sdk-nodejs.yml index 81586a5662..315331609b 100644 --- a/.github/workflows/sdk-nodejs.yml +++ b/.github/workflows/sdk-nodejs.yml @@ -25,13 +25,12 @@ permissions: jobs: nodejs: - name: "Node.js (${{ matrix.os }}, ${{ matrix.transport }})" + name: "Node.js (${{ matrix.os }}, CAPI)" if: contains(fromJSON('["all","linux-x64","darwin-arm64","win32-x64"]'), inputs.platform) strategy: fail-fast: false matrix: os: ${{ fromJSON(inputs.platform == 'linux-x64' && '["ubuntu-latest"]' || inputs.platform == 'darwin-arm64' && '["macos-latest"]' || inputs.platform == 'win32-x64' && '["windows-latest"]' || '["ubuntu-latest","macos-latest","windows-latest"]') }} - transport: ${{ fromJSON(github.event_name == 'merge_group' && '["default"]' || '["default","inprocess"]') }} runs-on: ${{ matrix.os }} defaults: run: @@ -42,9 +41,9 @@ jobs: timeout-minutes: 4 with: persist-credentials: false - - uses: actions/download-artifact@v8 + - uses: ./.github/actions/download-sdk-runtime with: - name: ${{ runner.os == 'Linux' && format('executable-sdk-{0}-{1}-gnu', runner.os, runner.arch) || format('executable-sdk-{0}-{1}', runner.os, runner.arch) }} + artifact-name: ${{ runner.os == 'Linux' && format('executable-sdk-{0}-{1}-gnu', runner.os, runner.arch) || format('executable-sdk-{0}-{1}', runner.os, runner.arch) }} path: ${{ github.workspace }} - run: node "$SDK_HOME/scripts/ci/runtime-artifact.mjs" restore && node "$SDK_HOME/scripts/ci/runtime-artifact.mjs" prepare working-directory: . @@ -59,14 +58,14 @@ jobs: working-directory: ${{ inputs.sdk-home }}/scripts/codegen # TEMPORARY MERGE QUEUE REDUCTION: pull requests and main pushes retain # static checks; merge groups rerun only SDK compatibility coverage. - - if: github.event_name != 'merge_group' && runner.os == 'Linux' && matrix.transport == 'default' + - if: github.event_name != 'merge_group' && runner.os == 'Linux' run: npm run format:check - - if: github.event_name != 'merge_group' && runner.os == 'Linux' && matrix.transport == 'default' + - if: github.event_name != 'merge_group' && runner.os == 'Linux' run: npm run lint - - if: github.event_name != 'merge_group' && runner.os == 'Linux' && matrix.transport == 'default' + - if: github.event_name != 'merge_group' && runner.os == 'Linux' run: npm run typecheck - run: npm run build - - if: runner.os == 'Linux' && matrix.transport == 'default' + - if: runner.os == 'Linux' env: COPILOT_SDK_RUNTIME_PLATFORMS: linux-x64 run: | @@ -74,30 +73,29 @@ jobs: npm run verify:release-packages - run: npm ci --ignore-scripts working-directory: ${{ inputs.sdk-home }}/test/harness - - if: runner.os == 'Linux' && matrix.transport == 'default' + - if: runner.os == 'Linux' run: npm test working-directory: ${{ inputs.sdk-home }}/test/harness - - if: runner.os == 'Linux' && matrix.transport == 'default' + - if: runner.os == 'Linux' run: npm ci working-directory: ${{ inputs.sdk-home }}/scripts/corrections - - if: runner.os == 'Linux' && matrix.transport == 'default' + - if: runner.os == 'Linux' run: npm test working-directory: ${{ inputs.sdk-home }}/scripts/corrections - if: runner.os == 'Windows' run: pwsh.exe -Command "Write-Host 'PowerShell ready'" - - env: + - name: Test out-of-process + id: subprocess + env: COPILOT_HMAC_KEY: ${{ secrets.COPILOT_DEVELOPER_CLI_INTEGRATION_HMAC_KEY }} - COPILOT_SDK_DEFAULT_CONNECTION: ${{ matrix.transport == 'inprocess' && 'inprocess' || '' }} run: | - if [ "$COPILOT_SDK_DEFAULT_CONNECTION" = "inprocess" ]; then - npm test -- test/e2e/inprocess_ffi.e2e.test.ts test/e2e/auth_host.e2e.test.ts - elif [ "$GITHUB_EVENT_NAME" = "merge_group" ]; then + if [ "$GITHUB_EVENT_NAME" = "merge_group" ]; then npm test -- --reporter=default --reporter=json --outputFile="$RUNNER_TEMP/sdk-nodejs-results.json" else npm test fi - name: Upload merge-queue Node.js test results - if: always() && github.event_name == 'merge_group' && runner.os == 'Linux' && matrix.transport == 'default' + if: always() && github.event_name == 'merge_group' && runner.os == 'Linux' continue-on-error: true uses: actions/upload-artifact@v7 with: @@ -105,23 +103,67 @@ jobs: path: ${{ runner.temp }}/sdk-nodejs-results.json if-no-files-found: warn retention-days: 7 + - name: Test in-process smoke + if: ${{ !cancelled() && github.event_name != 'merge_group' && (steps.subprocess.outcome == 'success' || steps.subprocess.outcome == 'failure') }} + env: + COPILOT_HMAC_KEY: ${{ secrets.COPILOT_DEVELOPER_CLI_INTEGRATION_HMAC_KEY }} + COPILOT_SDK_DEFAULT_CONNECTION: inprocess + run: npm test -- test/e2e/inprocess_ffi.e2e.test.ts test/e2e/auth_host.e2e.test.ts - nodejs-musl-x64: - name: "Node.js (Alpine x64, ${{ matrix.transport }})" - if: inputs.platform == 'all' || inputs.platform == 'linuxmusl-x64' + # Keep each BYOK backend on its own runner; shared runtime E2E coverage belongs + # in TypeScript rather than repeating these sweeps across SDK languages. + nodejs-backends: + name: "Node.js (ubuntu-latest, ${{ matrix.backend }})" + if: github.event_name != 'merge_group' && (inputs.platform == 'all' || inputs.platform == 'linux-x64') runs-on: ubuntu-latest + timeout-minutes: 30 strategy: fail-fast: false matrix: - transport: [default, inprocess] + backend: [anthropic-messages, openai-responses, openai-completions] + defaults: + run: + shell: bash + working-directory: ${{ inputs.sdk-home }}/nodejs + env: + COPILOT_SDK_E2E_BACKEND: ${{ matrix.backend }} + steps: + - uses: actions/checkout@v7 + timeout-minutes: 4 + with: + persist-credentials: false + - uses: ./.github/actions/download-sdk-runtime + with: + artifact-name: executable-sdk-Linux-X64-gnu + path: ${{ github.workspace }} + - run: node "$SDK_HOME/scripts/ci/runtime-artifact.mjs" restore && node "$SDK_HOME/scripts/ci/runtime-artifact.mjs" prepare + working-directory: . + - uses: actions/setup-node@v6 + with: + node-version: 22 + cache: npm + cache-dependency-path: ${{ inputs.sdk-home }}/nodejs/package-lock.json + - run: npm ci --ignore-scripts + - run: npm run build + - run: npm ci --ignore-scripts + working-directory: ${{ inputs.sdk-home }}/test/harness + - name: Test out-of-process E2Es + env: + COPILOT_HMAC_KEY: ${{ secrets.COPILOT_DEVELOPER_CLI_INTEGRATION_HMAC_KEY }} + run: npm test -- test/e2e + + nodejs-musl-x64: + name: "Node.js (Alpine x64, CAPI)" + if: inputs.platform == 'all' || inputs.platform == 'linuxmusl-x64' + runs-on: ubuntu-latest steps: - uses: actions/checkout@v7 timeout-minutes: 4 with: persist-credentials: false - - uses: actions/download-artifact@v8 + - uses: ./.github/actions/download-sdk-runtime with: - name: executable-sdk-Linux-X64-musl + artifact-name: executable-sdk-Linux-X64-musl path: ${{ github.workspace }} - run: node "$SDK_HOME/scripts/ci/runtime-artifact.mjs" restore working-directory: . @@ -132,7 +174,6 @@ jobs: image: node:22-alpine sdk-root: /workspace/${{ inputs.sdk-home }} workdir: /workspace/${{ inputs.sdk-home }}/nodejs - transport: ${{ matrix.transport }} command: | npm ci --ignore-scripts (cd "$COPILOT_SDK_ROOT/scripts/codegen" && npm ci --ignore-scripts) @@ -152,11 +193,10 @@ jobs: esac test -x "$COPILOT_CLI_PATH" test -f "$(dirname "$COPILOT_CLI_PATH")/runtime.node" - if [ "$COPILOT_SDK_TEST_TRANSPORT" = "inprocess" ]; then - npm test -- test/e2e/inprocess_ffi.e2e.test.ts test/e2e/auth_host.e2e.test.ts - else - npm test - fi + status=0 + npm test || status=$? + COPILOT_SDK_DEFAULT_CONNECTION=inprocess npm test -- test/e2e/inprocess_ffi.e2e.test.ts test/e2e/auth_host.e2e.test.ts || status=$? + exit "$status" docs-typescript: name: "Docs (TypeScript)" diff --git a/.github/workflows/sdk-python.yml b/.github/workflows/sdk-python.yml index fa56ff533e..71bedf2eaa 100644 --- a/.github/workflows/sdk-python.yml +++ b/.github/workflows/sdk-python.yml @@ -25,13 +25,12 @@ permissions: jobs: python: - name: "Python (${{ matrix.os }}, ${{ matrix.transport }})" + name: "Python (${{ matrix.os }})" if: contains(fromJSON('["all","linux-x64","darwin-arm64","win32-x64"]'), inputs.platform) strategy: fail-fast: false matrix: os: ${{ fromJSON(inputs.platform == 'linux-x64' && '["ubuntu-latest"]' || inputs.platform == 'darwin-arm64' && '["macos-latest"]' || inputs.platform == 'win32-x64' && '["windows-latest"]' || '["ubuntu-latest","macos-latest","windows-latest"]') }} - transport: [default, inprocess] runs-on: ${{ matrix.os }} timeout-minutes: 20 defaults: @@ -45,9 +44,9 @@ jobs: timeout-minutes: 4 with: persist-credentials: false - - uses: actions/download-artifact@v8 + - uses: ./.github/actions/download-sdk-runtime with: - name: ${{ runner.os == 'Linux' && format('executable-sdk-{0}-{1}-gnu', runner.os, runner.arch) || format('executable-sdk-{0}-{1}', runner.os, runner.arch) }} + artifact-name: ${{ runner.os == 'Linux' && format('executable-sdk-{0}-{1}-gnu', runner.os, runner.arch) || format('executable-sdk-{0}-{1}', runner.os, runner.arch) }} path: ${{ github.workspace }} - run: node "$SDK_HOME/scripts/ci/runtime-artifact.mjs" restore && node "$SDK_HOME/scripts/ci/runtime-artifact.mjs" prepare working-directory: . @@ -63,52 +62,50 @@ jobs: - run: uv sync --locked --all-extras --dev # TEMPORARY MERGE QUEUE REDUCTION: pull requests and main pushes retain # static checks; merge groups rerun only SDK compatibility coverage. - - if: github.event_name != 'merge_group' && runner.os == 'Linux' && matrix.transport == 'default' + - if: github.event_name != 'merge_group' && runner.os == 'Linux' run: uv run --locked ruff format --check . - - if: github.event_name != 'merge_group' && runner.os == 'Linux' && matrix.transport == 'default' + - if: github.event_name != 'merge_group' && runner.os == 'Linux' run: uv run --locked ruff check - - if: github.event_name != 'merge_group' && runner.os == 'Linux' && matrix.transport == 'default' + - if: github.event_name != 'merge_group' && runner.os == 'Linux' run: uv run --locked ty check copilot - run: npm ci --ignore-scripts working-directory: ${{ inputs.sdk-home }}/test/harness - if: runner.os == 'Windows' run: pwsh.exe -Command "Write-Host 'PowerShell ready'" - - env: + - name: Test out-of-process + id: subprocess + env: + COPILOT_HMAC_KEY: ${{ secrets.COPILOT_DEVELOPER_CLI_INTEGRATION_HMAC_KEY }} + run: env -u COPILOT_SKIP_CLI_DOWNLOAD uv run --locked pytest -v -s -n 2 --dist=loadfile + - name: Test in-process smoke + if: ${{ !cancelled() && (steps.subprocess.outcome == 'success' || steps.subprocess.outcome == 'failure') }} + env: COPILOT_HMAC_KEY: ${{ secrets.COPILOT_DEVELOPER_CLI_INTEGRATION_HMAC_KEY }} - COPILOT_SDK_DEFAULT_CONNECTION: ${{ matrix.transport == 'inprocess' && 'inprocess' || '' }} - run: | - if [ "$COPILOT_SDK_DEFAULT_CONNECTION" = "inprocess" ]; then - uv run --locked pytest -v -s e2e/test_inprocess_ffi_e2e.py e2e/test_auth_host_e2e.py - else - env -u COPILOT_SKIP_CLI_DOWNLOAD uv run --locked pytest -v -s -n 2 --dist=loadfile - fi + COPILOT_SDK_DEFAULT_CONNECTION: inprocess + run: uv run --locked pytest -v -s e2e/test_inprocess_ffi_e2e.py e2e/test_auth_host_e2e.py - name: Upload Python test diagnostics if: failure() uses: actions/upload-artifact@v7 with: - name: python-test-diagnostics-${{ matrix.os }}-${{ matrix.transport }}-${{ github.run_attempt }} + name: python-test-diagnostics-${{ matrix.os }}-${{ github.run_attempt }} path: ${{ inputs.sdk-home }}/python/.pytest-diagnostics/ include-hidden-files: true if-no-files-found: warn retention-days: 7 python-musl-x64: - name: "Python (Alpine x64, ${{ matrix.transport }})" + name: "Python (Alpine x64)" if: inputs.platform == 'all' || inputs.platform == 'linuxmusl-x64' runs-on: ubuntu-latest timeout-minutes: 20 - strategy: - fail-fast: false - matrix: - transport: [default, inprocess] steps: - uses: actions/checkout@v7 timeout-minutes: 4 with: persist-credentials: false - - uses: actions/download-artifact@v8 + - uses: ./.github/actions/download-sdk-runtime with: - name: executable-sdk-Linux-X64-musl + artifact-name: executable-sdk-Linux-X64-musl path: ${{ github.workspace }} - run: node "$SDK_HOME/scripts/ci/runtime-artifact.mjs" restore working-directory: . @@ -119,10 +116,9 @@ jobs: image: python:3.11-alpine sdk-root: /workspace/${{ inputs.sdk-home }} workdir: /workspace/${{ inputs.sdk-home }} - transport: ${{ matrix.transport }} command: | apk add --no-cache nodejs npm - python -m pip install --no-cache-dir uv + python -m pip install --no-cache-dir --timeout 120 uv npm --prefix "$COPILOT_SDK_ROOT/nodejs" ci --ignore-scripts npm --prefix "$COPILOT_SDK_ROOT/test/harness" ci --ignore-scripts @@ -140,16 +136,15 @@ jobs: cd "$COPILOT_SDK_ROOT/python" uv sync --locked --all-extras --dev - if [ "$COPILOT_SDK_TEST_TRANSPORT" = "inprocess" ]; then - uv run --locked pytest -v -s e2e/test_inprocess_ffi_e2e.py e2e/test_auth_host_e2e.py - else - env -u COPILOT_SKIP_CLI_DOWNLOAD uv run --locked pytest -v -s -n 2 --dist=loadfile - fi + status=0 + env -u COPILOT_SKIP_CLI_DOWNLOAD uv run --locked pytest -v -s -n 2 --dist=loadfile || status=$? + COPILOT_SDK_DEFAULT_CONNECTION=inprocess uv run --locked pytest -v -s e2e/test_inprocess_ffi_e2e.py e2e/test_auth_host_e2e.py || status=$? + exit "$status" - name: Upload Python test diagnostics if: failure() uses: actions/upload-artifact@v7 with: - name: python-test-diagnostics-alpine-x64-${{ matrix.transport }}-${{ github.run_attempt }} + name: python-test-diagnostics-alpine-x64-${{ github.run_attempt }} path: ${{ inputs.sdk-home }}/python/.pytest-diagnostics/ include-hidden-files: true if-no-files-found: warn diff --git a/.github/workflows/sdk-runtime-artifact.yml b/.github/workflows/sdk-runtime-artifact.yml index 08d8d0e3dd..0481e945ae 100644 --- a/.github/workflows/sdk-runtime-artifact.yml +++ b/.github/workflows/sdk-runtime-artifact.yml @@ -4,6 +4,8 @@ env: COPILOT_AUTO_UPDATE: "false" COPILOT_BAZEL_ANG_ENABLED: ${{ vars.COPILOT_BAZEL_ANG_ENABLED }} COPILOT_BAZEL_ANG_SCOPE: ${{ vars.COPILOT_BAZEL_ANG_SCOPE }} + COPILOT_BAZEL_ANG_PR_SAMPLE_PERCENTAGE: ${{ vars.COPILOT_BAZEL_ANG_PR_SAMPLE_PERCENTAGE }} + COPILOT_BAZEL_ANG_PR_SAMPLED_MODE: ${{ vars.COPILOT_BAZEL_ANG_PR_SAMPLED_MODE }} COPILOT_RUNTIME_E2E_TEST_HOOKS: "1" CARGO_PROFILE_DEV_DEBUG: line-tables-only GIT_HTTP_LOW_SPEED_LIMIT: 1000 @@ -53,13 +55,96 @@ on: permissions: contents: read +concurrency: + group: ${{ inputs.shared-cli-build-compatible && format('project-build-sea-{0}-release-{1}-{2}', inputs.os, inputs.bazel-config, github.sha) || format('sdk-runtime-artifact-{0}-{1}', inputs.target, github.sha) }} + queue: max + jobs: + cache-relay: + if: inputs.runtime-source == 'checkout' + runs-on: ubuntu-slim + timeout-minutes: 15 + permissions: + actions: read + contents: read + outputs: + cache-hit: ${{ steps.cache.outputs.cache-hit }} + steps: + - uses: actions/checkout@v7 + timeout-minutes: 4 + with: + persist-credentials: false + + - uses: actions/setup-node@v6 + with: + node-version-file: .nvmrc + + - name: Capture verified addon source identity + run: node --experimental-strip-types script/resolve-cli-build-identity.ts --github-env + + - name: Restore completed runtime build + id: cache + uses: actions/cache/restore@v6 + with: + path: | + project-build-receipt.txt + dist-cli/ + dist-tmp/release-downloads/ripgrep/ + dist-tmp/release-downloads/tgrep/ + dist-bin/ + dist-pkg-tarballs/ + src/core/sharedApi/runtime-generated/index.d.ts + src/native/runtime/runtime.*.node.metadata + src/native/runtime/copilot-runtime.*.profile + src/native/runtime/index.d.ts + src/native/cli/index.d.ts + !dist-cli/sea.blob + !dist-cli/github-copilot-*.tgz + key: ${{ inputs.shared-cli-build-compatible && format('project-build-v4-sea-{0}-{1}-release-{2}-{3}-{4}', inputs.os, inputs.target, inputs.bazel-config, inputs.use-bazel-addons-on-darwin, github.sha) || format('sdk-project-build-v4-{0}-{1}-{2}-release-{3}-{4}-{5}', inputs.target, inputs.os, inputs.target, inputs.bazel-config, inputs.use-bazel-addons-on-darwin, github.sha) }} + enableCrossOsArchive: "true" + + - name: Restore cached source-layout addons and executable bits + if: steps.cache.outputs.cache-hit == 'true' + run: node script/restore-cli-build.mjs + + - name: Verify completed runtime build + if: steps.cache.outputs.cache-hit == 'true' + shell: bash + env: + TARGET: ${{ inputs.target }} + run: | + extension= + if [[ "$TARGET" == win32-* ]]; then + extension=.exe + fi + test -s project-build-receipt.txt + test -s "dist-cli/prebuilds/$TARGET/runtime.node" + test -s "dist-cli/prebuilds/$TARGET/cli-native.node" + test -s "dist-cli/prebuilds/$TARGET/copilot-runtime$extension" + test -s "dist-bin/$TARGET/copilot$extension" + + - name: Upload SDK runtime artifact + if: steps.cache.outputs.cache-hit == 'true' + uses: actions/upload-artifact@v7 + with: + name: executable-sdk-${{ inputs.artifact }} + path: | + dist-cli/ + dist-bin/${{ inputs.target }}/ + !dist-cli/sea.blob + !dist-cli/github-copilot-*.tgz + include-hidden-files: true + if-no-files-found: error + retention-days: 1 + build: + needs: cache-relay + if: >- + !cancelled() && + (inputs.runtime-source == 'published' || + (needs.cache-relay.result == 'success' && needs.cache-relay.outputs.cache-hit != 'true')) runs-on: ${{ inputs.runtime-source == 'checkout' && inputs.os || 'ubuntu-latest' }} timeout-minutes: ${{ inputs.timeout-minutes }} - concurrency: - group: ${{ inputs.shared-cli-build-compatible && format('project-build-sea-{0}-release-{1}-{2}', inputs.os, inputs.bazel-config, github.sha) || format('sdk-runtime-artifact-{0}-{1}', inputs.target, github.sha) }} - queue: max permissions: actions: read contents: read @@ -103,7 +188,8 @@ jobs: src/native/cli/index.d.ts !dist-cli/sea.blob !dist-cli/github-copilot-*.tgz - key: ${{ inputs.shared-cli-build-compatible && format('project-build-v3-sea-{0}-{1}-{2}-release-{3}-{4}-{5}', inputs.os, runner.os, runner.arch, inputs.bazel-config, inputs.use-bazel-addons-on-darwin, github.sha) || format('sdk-project-build-v3-{0}-{1}-{2}-{3}-release-{4}-{5}-{6}', inputs.target, inputs.os, runner.os, runner.arch, inputs.bazel-config, inputs.use-bazel-addons-on-darwin, github.sha) }} + key: ${{ inputs.shared-cli-build-compatible && format('project-build-v4-sea-{0}-{1}-release-{2}-{3}-{4}', inputs.os, inputs.target, inputs.bazel-config, inputs.use-bazel-addons-on-darwin, github.sha) || format('sdk-project-build-v4-{0}-{1}-{2}-release-{3}-{4}-{5}', inputs.target, inputs.os, inputs.target, inputs.bazel-config, inputs.use-bazel-addons-on-darwin, github.sha) }} + enableCrossOsArchive: "true" - name: Restore cached source-layout addons and executable bits if: inputs.runtime-source == 'checkout' && steps.cache.outputs.cache-hit == 'true' @@ -113,6 +199,10 @@ jobs: uses: ./.github/actions/setup-node timeout-minutes: ${{ fromJSON(env.SETUP_NODE_TIMEOUT_MINUTES) }} + - name: Capture verified addon source identity + if: inputs.runtime-source == 'checkout' && steps.cache.outputs.cache-hit != 'true' + run: node --experimental-strip-types script/resolve-cli-build-identity.ts --github-env + - if: inputs.runtime-source == 'checkout' && steps.cache.outputs.cache-hit != 'true' && inputs.prebuilt-addons-artifact == '' && !(runner.os == 'Windows' && inputs.target == 'win32-arm64') uses: ./.github/actions/setup-bazel with: @@ -191,6 +281,7 @@ jobs: !dist-cli/sea.blob !dist-cli/github-copilot-*.tgz key: ${{ steps.cache.outputs.cache-primary-key }} + enableCrossOsArchive: "true" - name: Upload SDK runtime artifact uses: actions/upload-artifact@v7 diff --git a/.github/workflows/sdk-rust.yml b/.github/workflows/sdk-rust.yml index 10b99a6beb..5ab3134139 100644 --- a/.github/workflows/sdk-rust.yml +++ b/.github/workflows/sdk-rust.yml @@ -25,7 +25,7 @@ permissions: jobs: rust: - name: "Rust (${{ matrix.os }}, default)" + name: "Rust (${{ matrix.os }})" if: contains(fromJSON('["all","linux-x64","darwin-arm64","win32-x64"]'), inputs.platform) timeout-minutes: 60 strategy: @@ -45,9 +45,9 @@ jobs: timeout-minutes: 4 with: persist-credentials: false - - uses: actions/download-artifact@v8 + - uses: ./.github/actions/download-sdk-runtime with: - name: ${{ runner.os == 'Linux' && format('executable-sdk-{0}-{1}-gnu', runner.os, runner.arch) || format('executable-sdk-{0}-{1}', runner.os, runner.arch) }} + artifact-name: ${{ runner.os == 'Linux' && format('executable-sdk-{0}-{1}-gnu', runner.os, runner.arch) || format('executable-sdk-{0}-{1}', runner.os, runner.arch) }} path: ${{ github.workspace }} - run: node "$SDK_HOME/scripts/ci/runtime-artifact.mjs" restore && node "$SDK_HOME/scripts/ci/runtime-artifact.mjs" prepare working-directory: . @@ -86,78 +86,57 @@ jobs: working-directory: ${{ inputs.sdk-home }}/test/harness - if: runner.os == 'Windows' run: pwsh.exe -Command "Write-Host 'PowerShell ready'" - - env: + - name: Test out-of-process + id: subprocess + env: COPILOT_HMAC_KEY: ${{ secrets.COPILOT_DEVELOPER_CLI_INTEGRATION_HMAC_KEY }} RUST_E2E_CONCURRENCY: 4 run: cargo test --no-default-features --features runtime,test-support,derive -- --test-threads=4 --nocapture + - name: Report Linux Rust build resources + if: failure() && runner.os == 'Linux' + run: | + echo "::group::Rust SDK build resources" + df -h . "$RUNNER_TEMP" + df -i . "$RUNNER_TEMP" + free -h + if [ -d target ]; then + du -sh target + fi + echo "::endgroup::" - rust-inprocess: - name: "Rust (${{ matrix.os }}, inprocess)" - if: contains(fromJSON('["all","linux-x64","darwin-arm64"]'), inputs.platform) - strategy: - fail-fast: false - matrix: - os: ${{ fromJSON(inputs.platform == 'linux-x64' && '["ubuntu-latest"]' || inputs.platform == 'darwin-arm64' && '["macos-latest"]' || '["ubuntu-latest","macos-latest"]') }} - runs-on: ${{ matrix.os }} - defaults: - run: - shell: bash - working-directory: ${{ inputs.sdk-home }}/rust - env: - CARGO_TERM_COLOR: always - RUST_BACKTRACE: 1 - COPILOT_SDK_DEFAULT_CONNECTION: inprocess - steps: - - uses: actions/checkout@v7 - timeout-minutes: 4 - with: - persist-credentials: false - - uses: actions/download-artifact@v8 - with: - name: ${{ runner.os == 'Linux' && format('executable-sdk-{0}-{1}-gnu', runner.os, runner.arch) || format('executable-sdk-{0}-{1}', runner.os, runner.arch) }} - path: ${{ github.workspace }} - - run: node "$SDK_HOME/scripts/ci/runtime-artifact.mjs" restore && node "$SDK_HOME/scripts/ci/runtime-artifact.mjs" prepare - working-directory: . - - uses: dtolnay/rust-toolchain@stable - with: - toolchain: "1.94.0" - - uses: Swatinem/rust-cache@v2 - with: - workspaces: ${{ inputs.sdk-home }}/rust - prefix-key: v2-sdk-rust-no-bin - cache-bin: false - - run: npm ci --ignore-scripts - working-directory: ${{ inputs.sdk-home }}/test/harness - - env: + - name: Test in-process smoke + id: inprocess + if: ${{ !cancelled() && runner.os != 'Windows' && (steps.subprocess.outcome == 'success' || steps.subprocess.outcome == 'failure') }} + env: COPILOT_HMAC_KEY: ${{ secrets.COPILOT_DEVELOPER_CLI_INTEGRATION_HMAC_KEY }} + COPILOT_SDK_DEFAULT_CONNECTION: inprocess run: >- cargo test --no-default-features --features test-support,local-runtime,derive --test e2e inprocess::should_start_ping_and_stop_inprocess_client -- --exact --test-threads=1 --nocapture - - env: + - name: Test in-process authentication host + if: ${{ !cancelled() && (steps.inprocess.outcome == 'success' || steps.inprocess.outcome == 'failure') }} + env: COPILOT_HMAC_KEY: ${{ secrets.COPILOT_DEVELOPER_CLI_INTEGRATION_HMAC_KEY }} + COPILOT_SDK_DEFAULT_CONNECTION: inprocess run: >- cargo test --no-default-features --features test-support,local-runtime,derive --test e2e client::should_use_configured_github_host_for_authentication -- --exact --test-threads=1 --nocapture rust-musl-x64: - name: "Rust (Alpine x64, ${{ matrix.transport }})" + name: "Rust (Alpine x64)" if: inputs.platform == 'all' || inputs.platform == 'linuxmusl-x64' runs-on: ubuntu-latest timeout-minutes: 60 - strategy: - fail-fast: false - matrix: - transport: [default, inprocess] steps: - uses: actions/checkout@v7 timeout-minutes: 4 with: persist-credentials: false - - uses: actions/download-artifact@v8 + - uses: ./.github/actions/download-sdk-runtime with: - name: executable-sdk-Linux-X64-musl + artifact-name: executable-sdk-Linux-X64-musl path: ${{ github.workspace }} - run: node "$SDK_HOME/scripts/ci/runtime-artifact.mjs" restore working-directory: . @@ -172,7 +151,6 @@ jobs: image: rust:1.94.0-alpine sdk-root: /workspace/${{ inputs.sdk-home }} workdir: /workspace/${{ inputs.sdk-home }}/rust - transport: ${{ matrix.transport }} command: | rustc --print cfg | grep -Fqx "target_arch=\"x86_64\"" rustc --print cfg | grep -Fqx "target_env=\"musl\"" @@ -197,15 +175,13 @@ jobs: export CARGO_INCREMENTAL=0 export CARGO_PROFILE_TEST_DEBUG=0 cd "$COPILOT_SDK_ROOT/rust" - if [ "$COPILOT_SDK_TEST_TRANSPORT" = "inprocess" ]; then - unset RUST_E2E_CONCURRENCY - cargo test --no-default-features --features test-support,local-runtime,derive \ - --test e2e inprocess::should_start_ping_and_stop_inprocess_client \ - -- --exact --test-threads=1 --nocapture - cargo test --no-default-features --features test-support,local-runtime,derive \ - --test e2e client::should_use_configured_github_host_for_authentication \ - -- --exact --test-threads=1 --nocapture - else - export RUST_E2E_CONCURRENCY=4 - cargo test --no-default-features --features runtime,test-support,derive -- --test-threads=4 --nocapture - fi + status=0 + RUST_E2E_CONCURRENCY=4 cargo test --no-default-features --features runtime,test-support,derive -- --test-threads=4 --nocapture || status=$? + export COPILOT_SDK_DEFAULT_CONNECTION=inprocess + cargo test --no-default-features --features test-support,local-runtime,derive \ + --test e2e inprocess::should_start_ping_and_stop_inprocess_client \ + -- --exact --test-threads=1 --nocapture || status=$? + cargo test --no-default-features --features test-support,local-runtime,derive \ + --test e2e client::should_use_configured_github_host_for_authentication \ + -- --exact --test-threads=1 --nocapture || status=$? + exit "$status" diff --git a/.github/workflows/sdk.yml b/.github/workflows/sdk.yml index e21090824a..b6314e62f6 100644 --- a/.github/workflows/sdk.yml +++ b/.github/workflows/sdk.yml @@ -5,6 +5,8 @@ name: "SDK build and test" env: COPILOT_BAZEL_ANG_ENABLED: ${{ vars.COPILOT_BAZEL_ANG_ENABLED }} COPILOT_BAZEL_ANG_SCOPE: ${{ vars.COPILOT_BAZEL_ANG_SCOPE }} + COPILOT_BAZEL_ANG_PR_SAMPLE_PERCENTAGE: ${{ vars.COPILOT_BAZEL_ANG_PR_SAMPLE_PERCENTAGE }} + COPILOT_BAZEL_ANG_PR_SAMPLED_MODE: ${{ vars.COPILOT_BAZEL_ANG_PR_SAMPLED_MODE }} HUSKY: 0 POWERSHELL_UPDATECHECK: Off SETUP_NODE_TIMEOUT_MINUTES: 10 @@ -129,6 +131,9 @@ jobs: - name: Cross-build Windows x64 addons if: steps.cache.outputs.cache-hit != 'true' env: + # The CI-owned Bazel override is intentionally trusted; without this + # identity the producer stamps an empty source while consumers verify the SHA. + COPILOT_NAPI_ADDONS_BUILD_IDENTITY: ${{ github.sha }} CLI_GIT_COMMIT: ${{ github.sha }} CLI_PACKAGE_NAME: "@github/copilot" CLI_TARGET: win32-x64 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index e639b111a5..9e64a0bb04 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -101,12 +101,21 @@ download missing tools on first use. ### Prepare project dependencies Build tasks restore the selected SDK's dependencies: Node runs -`npm ci --ignore-scripts`, Python runs `uv sync --all-extras --dev`, and the +`npm ci --ignore-scripts --include=dev`, Python runs `uv sync --all-extras --dev`, and the other native build tools restore their project dependencies. In the runtime layout, build/test/generate tasks also install the codegen npm dependencies and refresh public schemas and the selected projections through Bazel. This preparation can update generated source files. +Node SDK builds and codegen preparation explicitly include development +dependencies even with `NODE_ENV=production` or `npm_config_omit=dev`, because +these packages provide the build tools. They skip installation when the npm +arguments, `package.json`, and `package-lock.json` match the last successful install's +`node_modules/.copilot-sdk-install-stamp`. Changing either file, removing +`node_modules` or its stamp, or an unsuccessful install requires a fresh install. +The runtime root's `build:sdk:link` uses the same stamp for Node dependencies. +Java codegen dependencies are installed only when Java is selected. + Tests and checks need additional tools that `pnpm install` does not provide. Before Node, Python, Go, .NET, or Rust SDK tests, prepare Node tooling and the shared replay harness if they are not already installed: @@ -142,6 +151,7 @@ Substitute `nodejs`, `python`, `go`, `dotnet`, `java`, or `rust` for ` | From the SDK root | Scope | | --- | --- | | `npm run build:` | Build one SDK; `npm run build` builds all six. | +| `npm run build:default` | Build the Node and Rust SDKs, in that order. | | `npm run test:` | Run that language's suite; `npm test` runs all six. | | `npm run test:default` | Node SDK unit tests and default-feature Rust SDK tests with `test-support`. | | `npm run check:` | Language-specific checks. Java includes `verify` (tests), .NET includes a solution build, and Rust includes Clippy and nightly formatting. | @@ -158,11 +168,17 @@ sources. Runtime-root aliases live in the parent runtime's SDK-local commands live in [package.json](package.json) and [run-tasks.mjs](scripts/run-tasks.mjs). +The runtime root's `pnpm run build` builds the CLI first, then invokes +`pnpm run build:sdk:default`. This default SDK profile prepares Node and Rust +projections together once and does not rebuild the CLI or build the other four SDKs. +Node declaration generation uses TypeScript's incremental build information in +`nodejs/dist/tsconfig.tsbuildinfo`; removing `dist` also clears that state. + In a runtime checkout, SDK tests request a current host `build:cli` before -running; Java and aggregate SDK builds also prepare the CLI. Unchanged Bazel +running; Java and all-six SDK builds also prepare the CLI. Unchanged Bazel actions remain cached. The Rust SDK build uses Bazel, but its tests use the independent SDK Cargo toolchain. Standalone tasks instead use the SDK's pinned -published runtime inputs. Do not work around a missing checkout artifact by +published runtime inputs and build Rust with Cargo's `--all-features`. Do not work around a missing checkout artifact by changing release pins or switching to a published runtime. Rust's `test:rust` and `test:default` run `cargo test --features test-support`; @@ -328,6 +344,32 @@ For TypeScript SDK E2Es, use the existing `nodejs/test/e2e/harness/sdkTestContext.ts` fixture. In the runtime repository, also follow the `e2e-test-author` skill's SDK section. +SDK CI runs full subprocess coverage followed by a short in-process smoke +step on the same runner wherever both modes share a platform. Smoke still +runs if the subprocess tests fail, and either failure fails the job. Java's +macOS, Windows, and musl smoke jobs remain separate because they have no +matching subprocess job. Merge groups retain the reduced Linux TypeScript +CAPI subprocess coverage. + +The three BYOK backend sweeps run in separate Linux TypeScript jobs, alongside +the normal CAPI job; they do not repeat unit tests, packaging, or static +checks. After preparing the runtime as described above, run a sweep from the +SDK root: + +```bash +COPILOT_SDK_E2E_BACKEND=anthropic-messages GITHUB_ACTIONS=true \ + npm --prefix nodejs test -- test/e2e +``` + +Use `openai-completions` or `openai-responses` for the other sweeps; unset the +variable or use `capi` for the normal suite. BYOK uses the shared captures +through the corresponding protocol adapter and never forwards replay misses +to a live provider. Use the fixture's `createClient()` for secondary clients +that create or resume model-backed sessions. Tests that require CAPI or own +their provider/request-handler setup stay in the normal job, with exclusions +in `vitest.config.ts` or individual `it.skipIf(isByokBackend)` cases. Other SDK +languages retain their language-specific provider tests in their normal suites. + The Node.js Vitest global setup bundles the shared replay proxy once per run into Vitest's project-owned temporary directory (and refreshes it on watch reruns). Each E2E context launches that bundle directly with the test runner's @@ -382,10 +424,12 @@ need copies across languages: Typed cases call the public idiomatic APIs: Node/Zod, C# generics, Python/Pydantic, Go generics, Java annotated records using the existing tool schema generator, -and Rust generics with `derive`/schemars. The tool/follow-up case also checks the -actual provider request's inferred schema, so a recorded JSON response alone -cannot mask missing schema forwarding. Explicit-schema and event-stream cases -exercise the corresponding raw public APIs instead. +and Rust generics with `derive`/schemars. The CAPI leg's tool/follow-up case also +checks the configured OpenAI provider request's inferred schema, so a recorded +JSON response alone cannot mask missing schema forwarding there. BYOK sweeps +retain the typed-result assertions without assuming the same wire-format +encoding. Explicit-schema and event-stream cases exercise the corresponding +raw public APIs instead. The existing suite in each language also checks rejection before admission and zero provider calls for oversized schemas and typed immediate steering. diff --git a/docs/features/fleet-mode.md b/docs/features/fleet-mode.md index 891a8ef04f..9a9547c8b9 100644 --- a/docs/features/fleet-mode.md +++ b/docs/features/fleet-mode.md @@ -243,7 +243,9 @@ This pattern gives every worker a clear owner and lets the parent session reason Fleet mode invokes sub-agents through the runtime's task mechanism. The runtime emits hook activity for sub-agent tool calls: the runtime 1.0.52 changelog notes that `preToolUse`, `postToolUse`, `subagentStart`, and `subagentStop` fire correctly for sub-agent tool calls. -A dedicated SDK hook callback for `subagentStart` or `subagentStop` was not found in the public SDK surface on this branch. SDK consumers can observe sub-agent activity through the generic session event stream, which includes events such as `subagent.started`, `subagent.completed`, `subagent.failed`, `subagent.selected`, and `subagent.deselected`. +All six SDKs expose typed sub-agent lifecycle hooks: Node.js uses `onSubagentStart` and `onSubagentStop`, Python and Rust use `on_subagent_start` and `on_subagent_stop`, Go and .NET use `OnSubagentStart` and `OnSubagentStop`, and Java uses `setOnSubagentStart` and `setOnSubagentStop`. Register them when creating or resuming a session. The start hook can prepend `additionalContext` to the child's first prompt; the stop hook can request another child turn with a block reason or replace the response reported to the parent. + +For passive status updates, subscribe to the generic session event stream. It includes `subagent.started`, `subagent.completed`, `subagent.failed`, `subagent.selected`, and `subagent.deselected`:
Node.js / TypeScript @@ -338,7 +340,7 @@ Keep plugin-provided sub-agent types narrow and descriptive so the orchestrator * Fleet mode is exposed through generated session RPC bindings and is marked experimental in several SDKs. * The SQL todos pattern is the canonical coordination model in the runtime guidance, but whether it is a stable extensibility contract for SDK consumers is still an open question. -* `subagentStart` and `subagentStop` are runtime hook names; this branch exposes sub-agent lifecycle to SDK consumers through the generic session event stream, not dedicated hook callbacks. +* Sub-agent lifecycle hook inputs identify the parent session but do not yet carry a shared tool-call ID for correlating with `subagent.*` events. * Plugin sub-agent registration is configured at the runtime layer through `--plugin-dir`; no SDK-level plugin registration helper was verified on this branch. * Java native typed bindings for `session.fleet.start` were not found in the Java SDK source on this branch. * Fleet mode does not remove the need for parent-agent review. Parallel workers can produce inconsistent assumptions that the orchestrator must reconcile. diff --git a/docs/features/streaming-events.md b/docs/features/streaming-events.md index 0292f98e00..514a8b2b0e 100644 --- a/docs/features/streaming-events.md +++ b/docs/features/streaming-events.md @@ -532,12 +532,31 @@ Emitted when a tool begins executing. ### `tool.execution_partial_result` -Ephemeral. Incremental output from a running tool (e.g., streaming bash output). +Deprecated. Use `tool.shell_output` for live terminal output. This ephemeral event remains emitted alongside the replacement during migration; subscribe to only one representation to avoid displaying the same output twice. | Data Field | Type | Required | Description | |------------|------|----------|-------------| | `toolCallId` | `string` | ✅ | Matches the corresponding `tool.execution_start` | -| `partialOutput` | `string` | ✅ | Incremental output chunk | +| `partialOutput` | `string` | ✅ | Bounded, cumulative replacement snapshot with stdout and stderr merged; not an append-only chunk | + +### `tool.shell_output` + +Ephemeral. Append-only output from an attached terminal command or a user-requested terminal command. Each event contains newly observed, secret-filtered text, without the separators or markers used to format final tool results for the model. + +| Data Field | Type | Required | Description | +|------------|------|----------|-------------| +| `toolCallId` | `string` | ✅ | Identifies the originating tool call | +| `text` | `string` | ✅ | New text to append, not a replacement snapshot | +| `stream` | `"stdout" \| "stderr" \| "terminal"` | | Output source; omitted means `"stdout"`. `"terminal"` identifies inherently merged PTY output, whose stdout/stderr origin cannot be recovered | +| `sequence` | `number` | ✅ | Starts at zero and increases across all streams for this tool call, in observed publication order | + +Accumulate `text` separately by `toolCallId` and `stream`, or join the chunks in sequence order for a combined display. A sequence number identifies duplicates and missing chunks; it does not establish exact ordering between independent operating-system writes. A late subscriber can receive a first sequence greater than zero. + +These events are live-only: they are not persisted or replayed after resume. Final `tool.execution_complete` results, model-facing formatting, and detached command capture are unchanged. Text can contain terminal control sequences; renderers must apply their normal terminal-text handling. + +An attached terminal command can keep running after `tool.execution_complete` when it runs asynchronously or continues beyond `initial_wait`. Output received while it continues uses the original `toolCallId` and continues the same `sequence`, rather than restarting at zero. Consumers that display only active calls must ignore chunks received after completion instead of recreating completed-call state. + +Secret filtering operates on each decoded chunk. The runtime does not artificially split lines, but operating-system reads can divide a line or secret across chunks; cross-chunk secret reconstruction is not provided. ### `tool.execution_progress` @@ -970,7 +989,7 @@ assistant.turn_start → Turn begins │ ├── permission.requested → Needs user approval │ ├── permission.completed → Approval result │ ├── tool.execution_start → Tool begins -│ ├── tool.execution_partial_result → Streaming tool output (ephemeral, repeated) +│ ├── tool.shell_output → Stream-tagged terminal output (ephemeral, repeated) │ ├── tool.execution_progress → Progress updates (ephemeral, repeated) │ ├── tool.execution_complete → Tool finished │ │ @@ -998,6 +1017,7 @@ This table lists key `data` payload fields. Common envelope fields are documente | `tool.user_requested` | | Tool | `toolCallId`, `toolName`, `arguments?` | | `tool.execution_start` | | Tool | `toolCallId`, `toolName`, `arguments?`, `mcpServerName?` | | `tool.execution_partial_result` | ✅ | Tool | `toolCallId`, `partialOutput` | +| `tool.shell_output` | ✅ | Tool | `toolCallId`, `text`, `stream?`, `sequence` | | `tool.execution_progress` | ✅ | Tool | `toolCallId`, `progressMessage` | | `tool.execution_complete` | | Tool | `toolCallId`, `success`, `result?`, `error?` | | `session.idle` | ✅ | Session | `aborted?` | diff --git a/docs/getting-started.md b/docs/getting-started.md index 5512da8531..b6172d6fc8 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -1843,6 +1843,14 @@ Copilot decides when to call your tool based on the user's question. When it doe 1. The result is sent back to Copilot 1. Copilot incorporates the result into its response +### Updating external tools in a running session + +The existing low-level `session.tools.set` request replaces the definitions owned by that SDK connection. Send the complete desired list (or an empty list to remove them), rather than a patch. The updated definitions apply to the next model request that reads them. During a tool round, a change processed before the post-tool refresh is visible on the next model call; a concurrent update processed after that refresh takes effect on a later request. It does **not** update the high-level SDK tool-handler map: register and manage handlers separately, or handle `external_tool.requested` and complete it with `session.tools.handlePendingToolCall`. + +On direct Anthropic API connections to `https://api.anthropic.com` using a model marked for Anthropic tool updates in the runtime's model catalog (currently Claude Fable 5 or 5.1, or Claude Opus 4.8, 5, or 5.5), the runtime can keep the original tool prefix and represent eligible between-turn SDK changes in provider-native conversation items to preserve a cacheable prefix. On the CAPI WebSocket Responses route for a model marked for Responses tool updates in that catalog (currently GPT-5.4 and 5.4 Mini, GPT-5.5, GPT-5.6 Luna, Sol, Terra, and Sol-fast, GPT-6 Astra, Sol, and Luna, and GPT-6.1 Sol), eligible append-only additions can use positioned `additional_tools` items. Eligible removals retain historical definitions and restrict callable tools with `tool_choice: { type: "allowed_tools", ... }`, or `"none"` when no tools remain. This removal optimization requires every callable survivor to be present in the original top-level tool list: CAPI rejects allowlist references to inline-added definitions when full history is resent. Removing an inline-added tool can preserve its historical position, but retaining a callable inline-added tool while removing another tool falls back to the complete current list. Schema replacements and unsupported tool-choice or provider-native tool configurations also use that fallback. + +Model support is an explicit catalog opt-in, independent of support for reasoning configuration updates, and other models should be enabled only after their route is verified. If historical changes no longer fit the prompt budget, or an optimized request receives a recognized pre-output provider rejection, the runtime returns to the current complete tool list. Anthropic through CAPI, unannotated OpenAI Responses models, other provider endpoints, mid-turn changes, and history rewrites use the current complete tool list. Removing every callable tool selects the runtime's existing single-shot HTTP path, which also uses the complete current list rather than this optimization. Successful compaction starts a new prefix with the tools currently provided; resuming in a new process does not replay previous tool changes. A stable request shape does not guarantee cached tokens or lower cost. + ## What's next? Now that you've got the basics, here are more powerful features to explore: diff --git a/docs/observability/opentelemetry.md b/docs/observability/opentelemetry.md index b3932ce66a..2078b0cfe5 100644 --- a/docs/observability/opentelemetry.md +++ b/docs/observability/opentelemetry.md @@ -112,6 +112,53 @@ let client = Client::start(ClientOptions::new() The OTLP protocol field configures the CLI's `"otlp-http"` exporter for all signals. Leave it unset to use the CLI default, or set it to `"http/protobuf"` to export protobuf over HTTP. +### Skill and command telemetry + +The runtime follows the [GenAI skill semantic conventions](https://github.com/open-telemetry/semantic-conventions-genai/commit/771e3210f7518694666834f7243573b8018c7a4f) +by refining existing `execute_tool` spans, not adding a separate skill +operation. A loader is named `execute_tool skill {skill}`. An unambiguously +identified resource or script is named +`execute_tool {tool} {skill} {resource}`, with a skill-relative resource name. +Other command spans append the actual known launcher name. + +* `gen_ai.skill.name` and `gen_ai.skill.resource.name`: Available independently of content capture +* `gen_ai.skill.description` and `gen_ai.skill.source.uri`: Require content capture through `TelemetryConfig` +* `process.executable.name`: The directly launched executable, such as the shell launcher, not a guessed command token +* `process.executable.path`: An absolute known path; requires content capture +* `process.exit.code`: An integer, including zero, when a command-executing tool reports an exit; unavailable exits and pre-spawn failures are omitted + +Poll tools do not inherit process attributes from the command they read. +Sandboxed executable identities are omitted when the sandbox backend does +not expose them. A nonzero exit does not imply that the tool itself failed. +Skill definitions and directory listings are not named skill resources. +Preloaded skills produce invocation events without a fabricated tool span. +Repeated preparation of the same selected-agent skill context emits one receipt; +changed content or provenance emits another. Child sessions report their own +preloads independently. Detached command spans identify the directly spawned +Unix `sh` supervisor or selected Windows PowerShell executable. + +Custom tools that override built-in names do not inherit native skill or process +attributes. Visible, enabled skills still own resources when model invocation +is disabled. Script attribution uses the post-hook command and the selected +shell's directory; it is omitted when startup scripts or profiles make the +execution context uncertain. + +While managed telemetry settings are unresolved, pending tracking buffers only +capture-independent tool facts. Descriptions, source URIs, and executable paths +observed before the policy resolves are omitted, even if capture is later enabled. +The final policy governs replay; disabled or cancelled tracking discards the buffer. + +These are breaking telemetry-name changes: use `gen_ai.skill.name` instead +of `github.copilot.tool.parameters.skill_name` or +`github.copilot.skill.name`, and use `gen_ai.skill.source.uri` instead of +`github.copilot.skill.path`. Local source values are file URIs, not bare paths. +The `github.copilot.skill.invoked` event and its content, source-category, +trigger, and plugin fields remain Copilot-specific. Skill content retains its +existing capture gate. + +The skill attributes and span refinements have Development stability; the +process attributes have Release Candidate stability. + ### Trace context propagation > **Most users don't need this.** The `TelemetryConfig` above is all you need to collect traces from the CLI. The trace context propagation described in this section is an **advanced feature** for applications that create their own OpenTelemetry spans and want them to appear in the **same distributed trace** as the CLI's spans. diff --git a/dotnet/README.md b/dotnet/README.md index 24f7d9b858..6bde71b71c 100644 --- a/dotnet/README.md +++ b/dotnet/README.md @@ -110,6 +110,7 @@ new CopilotClient(CopilotClientOptions? options = null) - `GitHubToken` - GitHub token for authentication. When provided, takes priority over other auth methods. - `UseLoggedInUser` - Whether to use logged-in user for authentication (default: true, but false when `GitHubToken` is provided). Cannot be used with `RuntimeConnection.ForUri(...)`. - `Telemetry` - OpenTelemetry configuration for the runtime process. Providing this enables telemetry — no separate flag needed. See [Telemetry](#telemetry) below. +- `SessionFs` - Custom session filesystem provider configuration. To serve provider-only images to the `view` tool, set `Capabilities.Binary` to `true` and return a provider implementing `ISessionFsBinaryProvider` (`ReadFileBytesAsync` and `WriteFileBytesAsync`) from `SessionConfig.CreateSessionFsProvider`. Image reads do not fall back to local files. Binary reads and writes are limited to 50,330,880 raw bytes (approximately 48 MiB); larger results return a filesystem error before encoding or decoding. - `InstallationConfirmationHandler` - Experimental connection-global human review for `installations.confirm`. Receives the typed request and one cancellation token that is cancelled when the request is retired or the connection closes, and returns an explicit decision. Does not enable installation capabilities. #### RuntimeConnection @@ -817,6 +818,16 @@ var session = await client.CreateSessionAsync(new SessionConfig }); ``` +An explicit `apply_patch` override may advertise a root string schema instead of +an object schema. The runtime delivers the patch as a string in +`ToolInvocation.Arguments`; the SDK binds that scalar to the named `input` +argument required by `AIFunction`. A schema wrapper around a typed handler should +therefore use a parameter named `input`. Object-schema tools retain their +declared parameter names. + +String-schema `apply_patch` overrides cannot contain JSON Schema references; +use an object schema if references are needed. + #### Skipping Permission Prompts Set `CopilotToolOptions.SkipPermission` to allow a tool to execute without triggering a permission prompt: @@ -995,6 +1006,8 @@ var session = await client.CreateSessionAsync(new SessionConfig Available section IDs are defined as static properties on the `SystemMessageSection` struct: `Preamble`, `Identity`, `Tone`, `ToolEfficiency`, `EnvironmentContext`, `CodeChangeRules`, `Guidelines`, `Safety`, `ToolInstructions`, `CustomInstructions`, `RuntimeInstructions`, `LastInstructions`. `Identity` and `ToolInstructions` are section groups that target a collection of related sub-sections as a unit; use `Preamble` to target just the identity preamble. +`SystemMessageSection.LastInstructions` (`last_instructions`) includes configured subagent-model guidance when the `task` tool is available. Removing or replacing this section also removes that guidance; a `SectionOverride.Transform` callback receives the complete section, including the guidance, and its returned content is authoritative. Append, prepend, and preserve retain their usual section semantics. These overrides change prompt prose only, not configured subagent models, tool availability, or runtime dispatch policy. `SystemMessageSection.RuntimeInstructions` is a separate section: removing it does not remove `SystemMessageSection.LastInstructions`. + Each section override supports five actions: `Replace`, `Remove`, `Append`, `Prepend`, and `Preserve` (a no-op that opts an individually-addressable section out of a group-level `Remove`). Unknown section IDs are handled gracefully: content is appended to additional instructions, and `Remove` overrides are silently ignored. #### Replace Mode @@ -1283,6 +1296,32 @@ var session = await client.CreateSessionAsync(new SessionConfig - `OnSessionStart` - Run logic when a session starts or resumes. - `OnSessionEnd` - Cleanup or logging when session ends. - `OnErrorOccurred` - Handle errors with retry/skip/abort strategies. +- `OnAgentStop` - Intercept a natural stop of the top-level agent. +- `OnSubagentStart` - Add context to a sub-agent before its first turn. +- `OnSubagentStop` - Inspect a sub-agent's last response, block the stop with a follow-up instruction, or replace its response. + +Sub-agent lifecycle hooks are registered on the parent session. Their input includes the parent `SessionId`, a `DateTimeOffset` timestamp, `WorkingDirectory`, `TranscriptPath` (empty if unavailable), and `AgentName`; `AgentDisplayName` and `AgentDescription` are optional. `OnSubagentStop` also receives an optional `AgentId`, `AgentType`, `StopReason` (currently `"end_turn"`), and the sub-agent's `Response`. `HookInvocation.SessionId` is the parent session ID. + +```csharp +var session = await client.CreateSessionAsync(new SessionConfig +{ + Hooks = new SessionHooks + { + OnSubagentStart = (input, invocation) => + Task.FromResult(new SubagentStartHookOutput + { + AdditionalContext = "Read the requested file before answering." + }), + OnSubagentStop = (input, invocation) => + Task.FromResult(new SubagentStopHookOutput + { + ModifiedResponse = input.Response.Trim() + }) + } +}); +``` + +`OnSubagentStart`'s `AdditionalContext` is prepended to the child's initial prompt. To request another child turn instead of accepting its response, return `new SubagentStopHookOutput { Decision = "block", Reason = "Explain your findings." }` from `OnSubagentStop`; a non-empty `Reason` is required. Otherwise, `ModifiedResponse` replaces the response reported to the parent. These hooks are distinct from `OnAgentStop`, which applies to the top-level agent. ## Elicitation Requests diff --git a/dotnet/src/Canvas.cs b/dotnet/src/Canvas.cs index 6bf8be984e..02874a79f2 100644 --- a/dotnet/src/Canvas.cs +++ b/dotnet/src/Canvas.cs @@ -171,6 +171,12 @@ public interface ICanvasHandler /// Handle a non-lifecycle action declared by the canvas. /// Default: throws . /// + /// + /// The returned value is sent to the model as the invoke_canvas_action + /// tool result. To return text and images, as tool handlers do, return a + /// with ; + /// any other value is rendered to the model as JSON text. + /// Task OnActionAsync(CanvasProviderInvokeActionRequest context, CancellationToken cancellationToken); } diff --git a/dotnet/src/Client.cs b/dotnet/src/Client.cs index bfa14884c4..999440992f 100644 --- a/dotnet/src/Client.cs +++ b/dotnet/src/Client.cs @@ -1208,7 +1208,9 @@ public async Task CreateSessionAsync(SessionConfig config, Cance config.Hooks.OnSessionStart != null || config.Hooks.OnSessionEnd != null || config.Hooks.OnErrorOccurred != null || - config.Hooks.OnAgentStop != null); + config.Hooks.OnAgentStop != null || + config.Hooks.OnSubagentStart != null || + config.Hooks.OnSubagentStop != null); var (wireSystemMessage, transformCallbacks) = ExtractTransformCallbacks(config.SystemMessage); @@ -1227,6 +1229,7 @@ public async Task CreateSessionAsync(SessionConfig config, Cance var registrationId = RegisterGitHubTokenProvider(config.GitHubTokenProvider); var registrationTransferred = false; CopilotSession? session = null; + string? serverAssignedSessionId = null; try { if (localSessionId != null) @@ -1343,6 +1346,7 @@ public async Task CreateSessionAsync(SessionConfig config, Cance && sessionIdProp.GetString() is string sessionId && !string.IsNullOrEmpty(sessionId)) { + serverAssignedSessionId = sessionId; session = InitializeSession( sessionId, connection.Rpc, @@ -1350,6 +1354,7 @@ public async Task CreateSessionAsync(SessionConfig config, Cance transformCallbacks, hasHooks, "CopilotClient.CreateSessionAsync"); + serverAssignedSessionId = null; } }; @@ -1391,6 +1396,22 @@ public async Task CreateSessionAsync(SessionConfig config, Cance catch (Exception ex) { session?.Unregister(); + if (localSessionId is null && serverAssignedSessionId is not null) + { + try + { + using var cleanupTimeout = new CancellationTokenSource(TimeSpan.FromSeconds(10)); + await DeleteSessionAsync(serverAssignedSessionId, cleanupTimeout.Token).ConfigureAwait(false); + } + catch (Exception cleanupError) when (cleanupError is OperationCanceledException + or IOException + or SocketException + or InvalidOperationException + or ObjectDisposedException) + { + _logger.LogWarning(cleanupError, "Failed to delete cloud session {SessionId} after creation failed", serverAssignedSessionId); + } + } if (ex is not OperationCanceledException) { @@ -1465,7 +1486,9 @@ public async Task ResumeSessionAsync(string sessionId, ResumeSes config.Hooks.OnSessionStart != null || config.Hooks.OnSessionEnd != null || config.Hooks.OnErrorOccurred != null || - config.Hooks.OnAgentStop != null); + config.Hooks.OnAgentStop != null || + config.Hooks.OnSubagentStart != null || + config.Hooks.OnSubagentStop != null); var (wireSystemMessage, transformCallbacks) = ExtractTransformCallbacks(config.SystemMessage); @@ -2196,6 +2219,11 @@ private void ConfigureSessionFsHandlers(CopilotSession session, FuncWhether the model supports provider-native thinking. Independent of configurable reasoning effort; omission means unknown. + [JsonPropertyName("thinking")] + public bool? Thinking { get; set; } + /// Whether this model supports canonical tool calling. [JsonPropertyName("toolCalls")] public bool? ToolCalls { get; set; } @@ -950,6 +962,234 @@ public sealed class SandboxHostSupport public bool Supported { get; set; } } +/// Status of the persistent certificate authority of the sandbox credential proxy. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class SandboxProxyCaStatus +{ + /// Whether this process can add the certificate authority to OS trust without credentials from a different user. False where OS trust is unsupported, and on Windows when the process cannot elevate itself to write the machine trust store. When false, do not offer to set up the certificate authority. + [JsonPropertyName("canInstall")] + public bool CanInstall { get; set; } + + /// Human-readable reason for the state. On `installed` or `notInstalled`, present only when the certificate authority must be rotated, and then says why. + [JsonPropertyName("detail")] + public string? Detail { get; set; } + + /// The state of the certificate authority. + [JsonPropertyName("state")] + public SandboxProxyCaState State { get; set; } +} + +/// Credential-injection capability flags applied while the sandbox is enabled. For the same capability independent of sandboxing, and matched to the credential's GitHub host, see `shell.credentials`; the two are additive. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class SandboxConfigAuth +{ + /// Whether to authenticate sandboxed gh through the local masking proxy. The child receives a fake GH_TOKEN; its real value is substituted only at github.com, api.github.com and uploads.github.com (github.com because gh repo clone authenticates git through gh auth git-credential). The repository's GitHub account takes precedence over the Copilot login. Default: false (opt-in). + [JsonPropertyName("gh")] + public bool? Gh { get; set; } + + /// Whether to authenticate sandboxed HTTPS git through the local masking proxy. The child receives a fake `http.<url>.extraheader`; the real Authorization header is substituted only at its original HTTPS host, port, and repository path scope. github.com uses the Copilot token; other forges use credentials resolved from the user's own helper on the host. Default: false (opt-in). + [JsonPropertyName("git")] + public bool? Git { get; set; } +} + +/// Destinations authorized to receive one masked environment credential. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class SandboxMaskedEnvVar +{ + /// Nonempty list of HTTPS injection hostnames or *.example.com patterns. Bare * is not accepted. These grants never override the sandbox network policy. Values in plaintext HTTP requests, URLs, bodies, encoded credentials, and signed requests are not substituted. + [JsonPropertyName("injectHosts")] + public IList InjectHosts { get => field ??= []; set; } +} + +/// Whole-value environment credential masking for sandboxed children. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class SandboxCredentialsConfig +{ + /// Environment variable names and their HTTPS injection destinations. Absent variables stay absent. No real values or sentinels are stored in this map. + [JsonPropertyName("envVars")] + public IDictionary EnvVars { get => field ??= new Dictionary(); set; } +} + +/// macOS seatbelt experimental options. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class SandboxConfigUserPolicyExperimentalSeatbelt +{ + /// Whether the macOS seatbelt profile may access the keychain. + [JsonPropertyName("keychainAccess")] + public bool? KeychainAccess { get; set; } +} + +/// Platform-specific experimental policy fields. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class SandboxConfigUserPolicyExperimental +{ + /// macOS seatbelt experimental options. + [JsonPropertyName("seatbelt")] + public SandboxConfigUserPolicyExperimentalSeatbelt? Seatbelt { get; set; } +} + +/// Filesystem rules to merge into the base policy. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class SandboxConfigUserPolicyFilesystem +{ + /// Whether to clear the policy when the session exits. + [JsonPropertyName("clearPolicyOnExit")] + public bool? ClearPolicyOnExit { get; set; } + + /// Paths explicitly denied. + [JsonPropertyName("deniedPaths")] + public IList? DeniedPaths { get; set; } + + /// Paths granted read-only access. + [JsonPropertyName("readonlyPaths")] + public IList? ReadonlyPaths { get; set; } + + /// Paths granted read/write access. + [JsonPropertyName("readwritePaths")] + public IList? ReadwritePaths { get; set; } +} + +/// HTTP proxy configuration for sandboxed traffic. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class SandboxConfigUserPolicyNetworkProxy +{ + /// Optional password for proxy authentication, combined with the URL at spawn time. The persisted value may be a literal password, a `${secret:…}` reference resolved from the OS keychain, or a `${VAR}`/`$VAR` environment reference; it is resolved just before the sandboxed process routes through the proxy. The /sandbox dialog stores a real password in the OS keychain and persists only a `${secret:…}` placeholder (never plaintext in settings.json); the field is masked in the dialog and redacted by /settings show. + [JsonPropertyName("password")] + public string? Password { get; set; } + + /// Proxy URL (e.g. http://proxy.example.com:8080). The port is optional and defaults to the scheme's standard port when omitted; an explicit port must be between 1 and 65535. Credentials must not be embedded here — a `user:pass@` authority is rejected; put them in the separate `username`/`password` fields. A credential-free http:// loopback proxy URL is routed through the localhost proxy automatically; loopback covers localhost and any *.localhost subdomain, the whole 127.0.0.0/8 range, ::1, and IPv4-mapped loopback (::ffff:127.0.0.1). An https:// URL, or one with a username/password set, is used as-is. + [JsonPropertyName("url")] + public string Url { get; set; } = string.Empty; + + /// Optional username for proxy authentication. Combined with the URL (and `password`) into `user:pass@host` when the sandboxed process routes through the proxy. + [JsonPropertyName("username")] + public string? Username { get; set; } +} + +/// Network rules to merge into the base policy. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class SandboxConfigUserPolicyNetwork +{ + /// Hosts allowed through the built-in sandbox proxy. A non-empty list denies unmatched hosts; an absent or empty list allows all hosts not blocked. Supports exact hostnames, IP addresses, and *.example.com for strict subdomains. Host rules do not override the outbound or local-network toggles. + [JsonPropertyName("allowedHosts")] + public IList? AllowedHosts { get; set; } + + /// Whether traffic to local/loopback addresses is allowed. + [JsonPropertyName("allowLocalNetwork")] + public bool? AllowLocalNetwork { get; set; } + + /// Whether outbound network traffic is allowed at all. + [JsonPropertyName("allowOutbound")] + public bool? AllowOutbound { get; set; } + + /// Hosts denied by the built-in sandbox proxy. Deny rules take precedence over allowedHosts. A domain also denies all its subdomains. IP addresses match exactly; *.example.com matches strict subdomains, and * denies every host. + [JsonPropertyName("blockedHosts")] + public IList? BlockedHosts { get; set; } + + /// HTTP(S) proxy for sandboxed traffic. This is the built-in local proxy's upstream: every sandboxed command reaches it through a loopback listener, so credentials stay in the runtime and never reach the child. On Windows the sandbox also needs local network access, because it reaches that listener over host loopback. Configure credentials in the separate username/password fields. The transient local listener URL is never persisted. + [JsonPropertyName("proxy")] + public SandboxConfigUserPolicyNetworkProxy? Proxy { get; set; } +} + +/// macOS seatbelt-specific options. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class SandboxConfigUserPolicySeatbelt +{ + /// Whether the macOS seatbelt profile may access the keychain. + [JsonPropertyName("keychainAccess")] + public bool? KeychainAccess { get; set; } +} + +/// User-managed sandbox policy fragment merged into the auto-discovered base policy. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class SandboxConfigUserPolicy +{ + /// Deprecated legacy location for `seatbelt`; read only when the top-level `seatbelt` is absent. + [JsonPropertyName("experimental")] + public SandboxConfigUserPolicyExperimental? Experimental { get; set; } + + /// Filesystem rules to merge into the base policy. + [JsonPropertyName("filesystem")] + public SandboxConfigUserPolicyFilesystem? Filesystem { get; set; } + + /// Network rules to merge into the base policy. + [JsonPropertyName("network")] + public SandboxConfigUserPolicyNetwork? Network { get; set; } + + /// macOS seatbelt options to merge into the base policy. + [JsonPropertyName("seatbelt")] + public SandboxConfigUserPolicySeatbelt? Seatbelt { get; set; } +} + +/// Resolved sandbox configuration. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class SandboxConfig +{ + /// Whether to auto-add the current working directory to readwritePaths. Default: true. + [JsonPropertyName("addCurrentWorkingDirectory")] + public bool? AddCurrentWorkingDirectory { get; set; } + + /// Whether the agent may request that an individual command run outside the sandbox, which the host then approves or denies through the usual permission flow. A host capability flag rather than part of the policy: it is stripped from the effective spawn policy and only has an effect while `enabled` is true. Fail-closed, unlike the opt-out flags on this object: omitting it offers no bypass. Default: false (opt-in). + [JsonPropertyName("allowBypass")] + public bool? AllowBypass { get; set; } + + /// Whether to auto-grant read access to tool directories discovered on PATH and in toolchain environment variables (GOROOT, JAVA_HOME, VIRTUAL_ENV, and similar), and to common developer-tool caches, config, and toolchains. Writable grants cover scratch caches, the Unix GitHub CLI cache, and Cargo's registry, git store, and lock/tracker files. A relocated CARGO_HOME gets the same narrow split: registry and git are read-write; bin is read-only; the home root, config.toml, and credentials.toml stay ungranted. Set to false to disable every grant listed above; user-installed toolchains and caches then need explicit userPolicy.filesystem readonlyPaths and readwritePaths entries. The working directory (see addCurrentWorkingDirectory), temporary storage, session log paths, and system locations follow their own rules and stay granted. Default: true (enabled by default; set to false to opt out). + [JsonPropertyName("allowDevToolAccess")] + public bool? AllowDevToolAccess { get; set; } + + /// Credential-injection capability flags. + [JsonPropertyName("auth")] + public SandboxConfigAuth? Auth { get; set; } + + /// Opt-in whole-value environment masking for sandboxed shell, MCP, and LSP children. Configured names get random sentinels; the local proxy substitutes them only in HTTPS request headers at their injection hosts. Approved bypasses skip masking and the sandbox proxy, so bypassed shells may receive the real environment values. Disabled or explicitly opted-out routes are not protected. No credential values are stored in this configuration. + [JsonPropertyName("credentials")] + public SandboxCredentialsConfig? Credentials { get; set; } + + /// Whether sandboxing is enabled for the session. + [JsonPropertyName("enabled")] + public bool Enabled { get; set; } + + /// The `sandboxLspServers` counterpart of `managedMcpRoutingLocked`. + [JsonInclude] + [JsonPropertyName("managedLspRoutingLocked")] + internal bool? ManagedLspRoutingLocked { get; set; } + + /// Set by the runtime when a managed policy forced `sandboxMcpServers` on and took the local opt-out away. Provenance rather than policy: it lets a sandbox startup failure point at the administrator instead of a setting the next managed merge would override, and it is ignored when comparing two configs for change. Only the managed merge may set it; a caller-supplied value is stripped. + [JsonInclude] + [JsonPropertyName("managedMcpRoutingLocked")] + internal bool? ManagedMcpRoutingLocked { get; set; } + + /// Whether language servers the session launches are confined by the sandbox. Only an explicit `false` opts out. Ignored while `enabled` is false. Default: true (enabled by default; set to false to opt out). + [JsonPropertyName("sandboxLspServers")] + public bool? SandboxLspServers { get; set; } + + /// Whether MCP servers the session launches are confined by the sandbox. Only an explicit `false` opts out; doing so also lets remote-MCP egress leave the sandbox, so the flag and `enabled` are always read together. Ignored while `enabled` is false. Default: true (enabled by default; set to false to opt out). + [JsonPropertyName("sandboxMcpServers")] + public bool? SandboxMcpServers { get; set; } + + /// User-managed sandbox policy fragment merged into the auto-discovered base policy. + [JsonPropertyName("userPolicy")] + public SandboxConfigUserPolicy? UserPolicy { get; set; } +} + +/// Identifies the credential hosts that the persistent certificate authority of the sandbox credential proxy must cover. The runtime always adds the hosts from the saved user settings. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +internal sealed class SandboxProxyCaRequest +{ + /// The sandbox configuration that the host gives its sessions. The runtime reads the credential hosts from `auth` and `credentials`; it ignores `enabled` and the other fields. + [JsonPropertyName("sandboxConfig")] + public SandboxConfig? SandboxConfig { get; set; } +} + +/// Result of creating the persistent certificate authority of the sandbox credential proxy. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class SandboxProxyCaCreateResult +{ + /// Absolute path of the public certificate of the certificate authority, in PEM format. + [JsonPropertyName("certificatePath")] + public string CertificatePath { get; set; } = string.Empty; +} + /// Built-in tool metadata with identifier, optional namespaced name, description, input-parameter schema, and usage instructions. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] public sealed class Tool @@ -7036,6 +7276,10 @@ public sealed class SessionFsSetProviderResult [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] public sealed class SessionFsSetProviderCapabilities { + /// Whether the provider supports binary reads and writes through sessionFs.readFileBytes and sessionFs.writeFileBytes. + [JsonPropertyName("binary")] + public bool? Binary { get; set; } + /// Whether the provider supports SQLite query/exists operations. [JsonPropertyName("sqlite")] public bool? Sqlite { get; set; } @@ -8639,83 +8883,6 @@ internal sealed class AgentRegistrySpawnRequest public AgentRegistrySpawnPermissionMode? PermissionMode { get; set; } } -/// Result of a OneAuth token acquisition. -/// Polymorphic base type discriminated by status. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -[JsonPolymorphic( - TypeDiscriminatorPropertyName = "status", - UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FallBackToBaseType)] -[JsonDerivedType(typeof(EntraTokenAcquireResultOk), "ok")] -[JsonDerivedType(typeof(EntraTokenAcquireResultInteractionRequired), "interaction-required")] -public partial class EntraTokenAcquireResult -{ - /// The type discriminator. - [JsonPropertyName("status")] - public virtual string Status { get; set; } = string.Empty; -} - - -/// The ok variant of . -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class EntraTokenAcquireResultOk : EntraTokenAcquireResult -{ - /// - [JsonIgnore] - public override string Status => "ok"; - - /// Opaque access token. - [JsonPropertyName("accessToken")] - public required string AccessToken { get; set; } - - /// Opaque OneAuth account id, when supplied by the broker. - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("accountId")] - public string? AccountId { get; set; } - - /// Expiry as milliseconds since Unix epoch, when supplied by OneAuth. - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("expiresOnTimestamp")] - public double? ExpiresOnTimestamp { get; set; } -} - -/// The interaction-required variant of . -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class EntraTokenAcquireResultInteractionRequired : EntraTokenAcquireResult -{ - /// - [JsonIgnore] - public override string Status => "interaction-required"; -} - -/// OneAuth token request supplied by a trusted host application. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class EntraTokenAcquireRequest -{ - /// Previously rejected token that OneAuth must bypass during renewal. - [JsonPropertyName("accessTokenToRenew")] - public string? AccessTokenToRenew { get; set; } - - /// Public client application id. - [JsonPropertyName("clientId")] - public string ClientId { get; set; } = string.Empty; - - /// Whether the broker may show interaction. - [JsonPropertyName("interaction")] - public EntraTokenInteraction Interaction { get; set; } - - /// Broker redirect URI registered for the client. Required: the OneAuth broker validates a non-empty, registered redirect URI for the public client (MSAL broker registration), so this is not a browser-flow vestige and cannot be omitted. - [JsonPropertyName("redirectUri")] - public string RedirectUri { get; set; } = string.Empty; - - /// Exact delegated scopes to request. - [JsonPropertyName("scopes")] - public IList Scopes { get => field ??= []; set; } - - /// Tenant id or tenant selector, such as common or organizations. - [JsonPropertyName("tenantId")] - public string TenantId { get; set; } = string.Empty; -} - /// Identifies the target session. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] internal sealed class SessionSuspendRequest @@ -9079,6 +9246,891 @@ internal sealed class LogRequest public string? Url { get; set; } } +/// Adapter-declared policy that tells clients whether discovery may run automatically. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class ModelProviderAutomaticDiscoveryPolicy +{ + /// Whether automatic discovery is allowed, limited to configured providers, or explicit-only. + [JsonPropertyName("mode")] + public ModelProviderAutomaticDiscoveryMode Mode { get; set; } + + /// Maximum network scope used by this adapter during discovery. + [JsonPropertyName("networkScope")] + public ModelProviderDiscoveryNetworkScope NetworkScope { get; set; } + + /// True when discovery requires non-null caller input. Omission or null is rejected before adapter execution. When false, omitted or null input selects adapter defaults without schema validation. + [JsonPropertyName("requiresInput")] + public bool RequiresInput { get; set; } + + /// True when the adapter must be enabled by a trusted owner, such as a trusted extension, before automatic discovery may run. + [JsonPropertyName("requiresTrust")] + public bool RequiresTrust { get; set; } +} + +/// An operation supported by a model-provider adapter. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class ModelProviderAdapterOperationDescriptor +{ + /// Optional self-contained JSON Schema Draft 7 for non-null discovery input. Only supported on discover. No external references are resolved. Omitted or null input selects defaults when requiresInput is false. Without a schema, the adapter validates supplied input. + [JsonPropertyName("inputSchema")] + public JsonElement? InputSchema { get; set; } + + /// Supported operation name: `discover`, `getStatus`, or `models.list`. Unknown names and duplicate declarations are rejected. + [JsonPropertyName("name")] + public string Name { get; set; } = string.Empty; +} + +/// Contributor attribution, independent of routing identity and authorization. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class ModelProviderAttribution +{ + /// Human-readable contributor name, not the adapter display name. + [JsonPropertyName("ownerDisplayName")] + public string? OwnerDisplayName { get; set; } + + /// Stable contributor identifier. Required and nonblank for extension and custom sources; optional for built-in and configured sources. Does not grant authority. + [JsonPropertyName("ownerId")] + public string? OwnerId { get; set; } + + /// Kind of component that supplied the adapter. Attribution does not confer authority. + [JsonPropertyName("source")] + public ModelProviderProvenanceSource Source { get; set; } +} + +/// A normalized model-provider adapter in the session's effective catalog. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class ModelProviderAdapterDescriptor +{ + /// Stable opaque identity for routing to this adapter. Unique in the effective catalog, independent of live registration generations. + [JsonPropertyName("adapterId")] + public string AdapterId { get; set; } = string.Empty; + + /// Adapter-declared policy for passive and automatic discovery. + [JsonPropertyName("automaticDiscovery")] + public ModelProviderAutomaticDiscoveryPolicy AutomaticDiscovery { get => field ??= new(); set; } + + /// Human-readable provider name. + [JsonPropertyName("displayName")] + public string DisplayName { get; set; } = string.Empty; + + /// Operations supported by this provider adapter. + [JsonPropertyName("operations")] + public IList Operations { get => field ??= []; set; } + + /// Attribution for the adapter itself. + [JsonPropertyName("provenance")] + public ModelProviderAttribution Provenance { get => field ??= new(); set; } + + /// Descriptive provider family, such as `ollama`. Different adapters may have the same family; use adapterId for routing. + [JsonPropertyName("providerKind")] + public string ProviderKind { get; set; } = string.Empty; +} + +/// Normalized model-provider adapter definitions available to the session, not discovered instances. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class ModelProviderAdapterCatalog +{ + /// Available provider adapters ordered by adapterId. + [JsonPropertyName("providers")] + public IList Providers { get => field ??= []; set; } +} + +/// Identifies the target session. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +internal sealed class SessionProvidersGetCatalogRequest +{ + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; +} + +/// Attribution for the adapter that produced a provider row. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class ModelProviderProvenance +{ + /// Stable opaque adapter identity from the effective catalog. Treat this as a whole identifier, not a parseable owner or kind. + [JsonPropertyName("adapterId")] + public string AdapterId { get; set; } = string.Empty; + + /// Human-readable contributor name, not the adapter display name. + [JsonPropertyName("ownerDisplayName")] + public string? OwnerDisplayName { get; set; } + + /// Stable contributor identifier when the adapter has an owner outside the runtime. Independent of the contribution mechanism and not a routing key. + [JsonPropertyName("ownerId")] + public string? OwnerId { get; set; } + + /// Descriptive provider family that produced this row; not a routing key. + [JsonPropertyName("providerKind")] + public string ProviderKind { get; set; } = string.Empty; + + /// Kind of component that supplied the adapter. + [JsonPropertyName("source")] + public ModelProviderProvenanceSource Source { get; set; } +} + +/// Serializable reference to a discovered provider instance. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class ModelProviderInstanceReference +{ + /// Stable opaque identity of the adapter that owns this reference. Must be present in the target session's effective catalog. + [JsonPropertyName("adapterId")] + public string AdapterId { get; set; } = string.Empty; + + /// Stable instance identifier derived by the provider adapter, such as `ollama:{normalizedEndpoint}`. + [JsonPropertyName("id")] + public string Id { get; set; } = string.Empty; + + /// Absolute provider management URI. The adapter validates normalization, supported schemes, and permission to access it against its bound configuration; a reference does not grant authority. + [Url] + [StringSyntax(StringSyntaxAttribute.Uri)] + [JsonPropertyName("managementEndpoint")] + public string ManagementEndpoint { get; set; } = string.Empty; + + /// Descriptive provider family. Must match the selected adapter; not a routing key. + [JsonPropertyName("providerKind")] + public string ProviderKind { get; set; } = string.Empty; +} + +/// A normalized model-provider instance discovered by the runtime. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class ModelProviderInstance +{ + /// Human-readable instance name. + [JsonPropertyName("displayName")] + public string DisplayName { get; set; } = string.Empty; + + /// Inference API endpoint when the provider exposes one separately from its management endpoint. + [Url] + [StringSyntax(StringSyntaxAttribute.Uri)] + [JsonPropertyName("inferenceEndpoint")] + public string? InferenceEndpoint { get; set; } + + /// Transport to use for inference against this instance. + [JsonPropertyName("inferenceTransport")] + public ProviderEndpointTransport? InferenceTransport { get; set; } + + /// Provider family to use for inference against this instance. + [JsonPropertyName("inferenceType")] + public ProviderEndpointType? InferenceType { get; set; } + + /// Wire API to use for inference against this instance, when required by the provider family. + [JsonPropertyName("inferenceWireApi")] + public ProviderEndpointWireApi? InferenceWireApi { get; set; } + + /// Attribution for the adapter that produced this instance. + [JsonPropertyName("provenance")] + public ModelProviderProvenance Provenance { get => field ??= new(); set; } + + /// Self-contained reference for subsequent provider operations. + [JsonPropertyName("reference")] + public ModelProviderInstanceReference Reference { get => field ??= new(); set; } +} + +/// Typed provider-operation outcome. Use the code for control flow and the optional message for display. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class ModelProviderOperationOutcome +{ + /// Machine-readable operation outcome. + [JsonPropertyName("code")] + public ModelProviderOperationOutcomeCode Code { get; set; } + + /// Human-readable detail for non-success outcomes. + [JsonPropertyName("message")] + public string? Message { get; set; } +} + +/// Provider instances found by a discovery operation. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class ModelProviderDiscoverResult +{ + /// Discovered provider instances. Empty when passive default discovery finds no reachable provider. + [JsonPropertyName("instances")] + public IList Instances { get => field ??= []; set; } + + /// Typed operation outcome. Passive discovery can return `absent` with an empty instance list. + [JsonPropertyName("outcome")] + public ModelProviderOperationOutcome Outcome { get => field ??= new(); set; } +} + +/// Provider discovery parameters. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +internal sealed class ModelProviderDiscoverRequest +{ + /// Opaque adapter identity returned by `session.providers.getCatalog`. + [JsonPropertyName("adapterId")] + public string AdapterId { get; set; } = string.Empty; + + /// Provider-specific JSON input. Omission or null selects adapter defaults unless requiresInput is true. Non-null input is validated against the advertised Draft 7 schema when present; otherwise validation belongs to the adapter. + [JsonPropertyName("input")] + public JsonElement? Input { get; set; } + + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; +} + +/// Current health information for a provider instance. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class ModelProviderStatus +{ + /// Normalized provider instance. + [JsonPropertyName("instance")] + public ModelProviderInstance Instance { get => field ??= new(); set; } + + /// Typed operation outcome. + [JsonPropertyName("outcome")] + public ModelProviderOperationOutcome Outcome { get => field ??= new(); set; } + + /// Open provider status value, such as `healthy`, `unreachable`, or `notInstalled`. + [JsonPropertyName("status")] + public string Status { get; set; } = string.Empty; + + /// Provider-reported version. + [JsonPropertyName("version")] + public string? Version { get; set; } +} + +/// Provider status request parameters. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +internal sealed class ModelProviderGetStatusRequest +{ + /// Provider instance reference returned by discovery. + [JsonPropertyName("instance")] + public ModelProviderInstanceReference Instance { get => field ??= new(); set; } + + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; +} + +/// Provider-reported model artifact metadata. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class ModelArtifactDetails +{ + /// Provider-reported model architecture. + [JsonPropertyName("architecture")] + public string? Architecture { get; set; } + + /// Provider-reported model families. + [JsonPropertyName("families")] + public IList? Families { get; set; } + + /// Primary model family. + [JsonPropertyName("family")] + public string? Family { get; set; } + + /// Artifact format, such as `gguf`. + [JsonPropertyName("format")] + public string? Format { get; set; } + + /// Provider-reported parameter count label. + [JsonPropertyName("parameterSize")] + public string? ParameterSize { get; set; } + + /// Provider-reported quantization label. + [JsonPropertyName("quantization")] + public string? Quantization { get; set; } + + /// Provider-reported tokenizer. + [JsonPropertyName("tokenizer")] + public string? Tokenizer { get; set; } +} + +/// A non-fatal provider observation warning. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class ModelProviderWarning +{ + /// Machine-readable warning code. + [JsonPropertyName("code")] + public string Code { get; set; } = string.Empty; + + /// Human-readable warning message. + [JsonPropertyName("message")] + public string Message { get; set; } = string.Empty; +} + +/// A model offered for agent conversations. Missing capability metadata does not disqualify a candidate. Models known to be incompatible, such as embedding-only models, are excluded by the adapter. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class DiscoveredModel +{ + /// Provider-reported model capabilities. Omitted capability fields are unknown; explicit false values are preserved. + [JsonPropertyName("capabilities")] + public ModelCapabilities Capabilities { get => field ??= new(); set; } + + /// Provider-reported model artifact details. + [JsonPropertyName("details")] + public ModelArtifactDetails Details { get => field ??= new(); set; } + + /// Provider-reported artifact digest. + [JsonPropertyName("digest")] + public string? Digest { get; set; } + + /// Provider-native model identifier. + [JsonPropertyName("id")] + public string Id { get; set; } = string.Empty; + + /// Provider-reported last-modified timestamp. + [JsonPropertyName("modifiedAt")] + public DateTimeOffset? ModifiedAt { get; set; } + + /// Provider-reported display name. + [JsonPropertyName("name")] + public string? Name { get; set; } + + /// Attribution for the adapter that produced this model row. + [JsonPropertyName("provenance")] + public ModelProviderProvenance Provenance { get => field ??= new(); set; } + + /// Provider-reported artifact size in bytes. + [JsonPropertyName("sizeBytes")] + public long? SizeBytes { get; set; } + + /// Non-fatal warnings encountered while enriching this model. + [JsonPropertyName("warnings")] + public IList Warnings { get => field ??= []; set; } +} + +/// Models offered for agent conversations by one provider instance. Adapters exclude known-incompatible models, but retain candidates with unknown capabilities. Listing does not guarantee compatibility. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class DiscoveredModelList +{ + /// Provider-native models in provider order. + [JsonPropertyName("models")] + public IList Models { get => field ??= []; set; } + + /// Typed operation outcome. + [JsonPropertyName("outcome")] + public ModelProviderOperationOutcome Outcome { get => field ??= new(); set; } +} + +/// Provider model inventory request parameters. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +internal sealed class ModelProviderModelsListRequest +{ + /// Provider instance reference returned by discovery. + [JsonPropertyName("instance")] + public ModelProviderInstanceReference Instance { get => field ??= new(); set; } + + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; +} + +/// Vision-specific limits. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class ModelCapabilitiesOverrideLimitsVision +{ + /// Maximum image size in bytes. + [JsonPropertyName("max_prompt_image_size")] + public long? MaxPromptImageSize { get; set; } + + /// Maximum number of images per prompt. + [JsonPropertyName("max_prompt_images")] + public long? MaxPromptImages { get; set; } + + /// MIME types the model accepts. + [JsonPropertyName("supported_media_types")] + public IList? SupportedMediaTypes { get; set; } +} + +/// Token limits for prompts, outputs, and context window. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class ModelCapabilitiesOverrideLimits +{ + /// Maximum total context window size in tokens. + [JsonPropertyName("max_context_window_tokens")] + public long? MaxContextWindowTokens { get; set; } + + /// Maximum number of output/completion tokens. + [JsonPropertyName("max_output_tokens")] + public long? MaxOutputTokens { get; set; } + + /// Maximum number of prompt/input tokens. + [JsonPropertyName("max_prompt_tokens")] + public long? MaxPromptTokens { get; set; } + + /// Vision-specific limits. + [JsonPropertyName("vision")] + public ModelCapabilitiesOverrideLimitsVision? Vision { get; set; } +} + +/// Feature flags indicating what the model supports. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class ModelCapabilitiesOverrideSupports +{ + /// Resolved Anthropic adaptive-thinking capability — unsupported / optional / required / adaptive_only. 'required' models reject thinking.type='enabled' with HTTP 400 but still accept 'disabled' (e.g. opus-4.7/4.8/5, sonnet-5); 'adaptive_only' models accept nothing but 'adaptive' (e.g. fable, mythos). + [JsonPropertyName("adaptive_thinking")] + public AdaptiveThinkingSupport? AdaptiveThinking { get; set; } + + /// Whether this model supports reasoning effort configuration. + [JsonPropertyName("reasoningEffort")] + public bool? ReasoningEffort { get; set; } + + /// Whether this model supports canonical tool calling. + [JsonPropertyName("toolCalls")] + public bool? ToolCalls { get; set; } + + /// Whether this model supports vision/image input. + [JsonPropertyName("vision")] + public bool? Vision { get; set; } +} + +/// Optional capability overrides (vision, tool_calls, reasoning, etc.). +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class ModelCapabilitiesOverride +{ + /// Token limits for prompts, outputs, and context window. + [JsonPropertyName("limits")] + public ModelCapabilitiesOverrideLimits? Limits { get; set; } + + /// Feature flags indicating what the model supports. + [JsonPropertyName("supports")] + public ModelCapabilitiesOverrideSupports? Supports { get; set; } +} + +/// RPC data type for ProtocolSystemMessageAppendConfig operations. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class ProtocolSystemMessageAppendConfig +{ + /// Text appended to the standard system prompt. + [JsonPropertyName("content")] + public string? Content { get; set; } + + /// Append-mode discriminator. Omission also selects append mode. + [JsonPropertyName("mode")] + public ProtocolAppendMode? Mode { get; set; } +} + +/// RPC data type for SystemMessageBlock operations. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class SystemMessageBlock +{ + /// Whether providers with explicit prompt caching should place a cache breakpoint after this block. + [JsonPropertyName("cacheBreakpoint")] + public bool? CacheBreakpoint { get; set; } + + /// Text content for this system-message block. + [JsonPropertyName("content")] + public string Content { get; set; } = string.Empty; + + /// Whether the block is static and may be cached independently of dynamic prompt content. + [JsonPropertyName("isStatic")] + public bool? IsStatic { get; set; } +} + +/// RPC data type for ProtocolSystemMessageReplaceConfig operations. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class ProtocolSystemMessageReplaceConfig +{ + /// Complete replacement system-message text. + [JsonPropertyName("content")] + public string Content { get; set; } = string.Empty; + + /// Optional structured blocks corresponding to the replacement content. + [JsonPropertyName("contentBlocks")] + public IList? ContentBlocks { get; set; } + + /// Replace-mode discriminator. + [JsonPropertyName("mode")] + public ProtocolReplaceMode Mode { get; set; } +} + +/// RPC data type for ProtocolStaticSectionOverride operations. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class ProtocolStaticSectionOverride +{ + /// Declarative operation applied to the section. + [JsonPropertyName("action")] + public ProtocolStaticSectionAction Action { get; set; } + + /// Optional content used by replace, append, and prepend operations. + [JsonPropertyName("content")] + public string? Content { get; set; } +} + +/// Polymorphic base type discriminated by action. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +[JsonPolymorphic( + TypeDiscriminatorPropertyName = "action", + UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FallBackToBaseType)] +[JsonDerivedType(typeof(ProtocolMarkerSectionOverrideTransform), "transform")] +[JsonDerivedType(typeof(ProtocolMarkerSectionOverridePreserve), "preserve")] +public partial class ProtocolMarkerSectionOverride +{ + /// The type discriminator. + [JsonPropertyName("action")] + public virtual string Action { get; set; } = string.Empty; +} + + +/// The transform variant of . +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public partial class ProtocolMarkerSectionOverrideTransform : ProtocolMarkerSectionOverride +{ + /// + [JsonIgnore] + public override string Action => "transform"; +} + +/// The preserve variant of . +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public partial class ProtocolMarkerSectionOverridePreserve : ProtocolMarkerSectionOverride +{ + /// + [JsonIgnore] + public override string Action => "preserve"; +} + +/// JSON union data type for ProtocolSectionOverride. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +[JsonConverter(typeof(Converter))] +public sealed partial class ProtocolSectionOverride +{ + /// Gets the value when this instance contains . + public ProtocolStaticSectionOverride? ProtocolStaticSectionOverride { get; } + + /// Gets the value when this instance contains . + public ProtocolMarkerSectionOverride? ProtocolMarkerSectionOverride { get; } + + /// Initializes a new instance of the class from . + public ProtocolSectionOverride(ProtocolStaticSectionOverride value) + { + ArgumentNullException.ThrowIfNull(value); + ProtocolStaticSectionOverride = value; + } + + /// Converts to . + public static implicit operator ProtocolSectionOverride(ProtocolStaticSectionOverride value) => new(value); + + /// Initializes a new instance of the class from . + public ProtocolSectionOverride(ProtocolMarkerSectionOverride value) + { + ArgumentNullException.ThrowIfNull(value); + ProtocolMarkerSectionOverride = value; + } + + /// Converts to . + public static implicit operator ProtocolSectionOverride(ProtocolMarkerSectionOverride value) => new(value); + + /// Provides a for serializing instances. + [EditorBrowsable(EditorBrowsableState.Never)] + public sealed class Converter : JsonConverter + { + /// + public override ProtocolSectionOverride Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + if (reader.TokenType == JsonTokenType.Null) + { + throw new JsonException("Expected JSON object for ProtocolSectionOverride."); + } + + using var document = JsonDocument.ParseValue(ref reader); + var element = document.RootElement; + if (element.ValueKind == JsonValueKind.Object && (element.TryGetProperty("action", out _) && element.GetProperty("action").ValueKind == JsonValueKind.String && (element.GetProperty("action").GetString() == "replace" || element.GetProperty("action").GetString() == "remove" || element.GetProperty("action").GetString() == "append" || element.GetProperty("action").GetString() == "prepend"))) + { + var protocolStaticSectionOverride = JsonSerializer.Deserialize(element, RpcJsonContext.Default.ProtocolStaticSectionOverride); + return protocolStaticSectionOverride is null ? throw new JsonException("Expected ProtocolStaticSectionOverride value.") : new ProtocolSectionOverride(protocolStaticSectionOverride); + } + if ((element.ValueKind == JsonValueKind.Object && (element.TryGetProperty("action", out _) && element.GetProperty("action").ValueKind == JsonValueKind.String && (element.GetProperty("action").GetString() == "transform")) || element.ValueKind == JsonValueKind.Object && (element.TryGetProperty("action", out _) && element.GetProperty("action").ValueKind == JsonValueKind.String && (element.GetProperty("action").GetString() == "preserve")))) + { + var protocolMarkerSectionOverride = JsonSerializer.Deserialize(element, RpcJsonContext.Default.ProtocolMarkerSectionOverride); + return protocolMarkerSectionOverride is null ? throw new JsonException("Expected ProtocolMarkerSectionOverride value.") : new ProtocolSectionOverride(protocolMarkerSectionOverride); + } + + throw new JsonException("JSON value did not match any ProtocolSectionOverride variant."); + } + + /// + public override void Write(Utf8JsonWriter writer, ProtocolSectionOverride value, JsonSerializerOptions options) + { + if (value.ProtocolStaticSectionOverride is { } protocolStaticSectionOverride) + { + JsonSerializer.Serialize(writer, protocolStaticSectionOverride, RpcJsonContext.Default.ProtocolStaticSectionOverride); + return; + } + if (value.ProtocolMarkerSectionOverride is { } protocolMarkerSectionOverride) + { + JsonSerializer.Serialize(writer, protocolMarkerSectionOverride, RpcJsonContext.Default.ProtocolMarkerSectionOverride); + return; + } + + throw new JsonException("No ProtocolSectionOverride variant value is set."); + } + } +} + +/// RPC data type for ProtocolSystemMessageCustomizeConfig operations. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class ProtocolSystemMessageCustomizeConfig +{ + /// Text appended after the customized sections. + [JsonPropertyName("content")] + public string? Content { get; set; } + + /// Customize-mode discriminator. + [JsonPropertyName("mode")] + public ProtocolCustomizeMode Mode { get; set; } + + /// Named standard-prompt section overrides. + [JsonPropertyName("sections")] + public IDictionary? Sections { get; set; } +} + +/// JSON union data type for ProtocolSystemMessageConfig. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +[JsonConverter(typeof(Converter))] +public sealed partial class ProtocolSystemMessageConfig +{ + /// Gets the value when this instance contains . + public ProtocolSystemMessageAppendConfig? ProtocolSystemMessageAppendConfig { get; } + + /// Gets the value when this instance contains . + public ProtocolSystemMessageReplaceConfig? ProtocolSystemMessageReplaceConfig { get; } + + /// Gets the value when this instance contains . + public ProtocolSystemMessageCustomizeConfig? ProtocolSystemMessageCustomizeConfig { get; } + + /// Initializes a new instance of the class from . + public ProtocolSystemMessageConfig(ProtocolSystemMessageAppendConfig value) + { + ArgumentNullException.ThrowIfNull(value); + ProtocolSystemMessageAppendConfig = value; + } + + /// Converts to . + public static implicit operator ProtocolSystemMessageConfig(ProtocolSystemMessageAppendConfig value) => new(value); + + /// Initializes a new instance of the class from . + public ProtocolSystemMessageConfig(ProtocolSystemMessageReplaceConfig value) + { + ArgumentNullException.ThrowIfNull(value); + ProtocolSystemMessageReplaceConfig = value; + } + + /// Converts to . + public static implicit operator ProtocolSystemMessageConfig(ProtocolSystemMessageReplaceConfig value) => new(value); + + /// Initializes a new instance of the class from . + public ProtocolSystemMessageConfig(ProtocolSystemMessageCustomizeConfig value) + { + ArgumentNullException.ThrowIfNull(value); + ProtocolSystemMessageCustomizeConfig = value; + } + + /// Converts to . + public static implicit operator ProtocolSystemMessageConfig(ProtocolSystemMessageCustomizeConfig value) => new(value); + + /// Provides a for serializing instances. + [EditorBrowsable(EditorBrowsableState.Never)] + public sealed class Converter : JsonConverter + { + /// + public override ProtocolSystemMessageConfig Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + if (reader.TokenType == JsonTokenType.Null) + { + throw new JsonException("Expected JSON object for ProtocolSystemMessageConfig."); + } + + using var document = JsonDocument.ParseValue(ref reader); + var element = document.RootElement; + if (element.ValueKind == JsonValueKind.Object && (!element.TryGetProperty("mode", out _) || (element.TryGetProperty("mode", out _) && element.GetProperty("mode").ValueKind == JsonValueKind.String && (element.GetProperty("mode").GetString() == "append")))) + { + var protocolSystemMessageAppendConfig = JsonSerializer.Deserialize(element, RpcJsonContext.Default.ProtocolSystemMessageAppendConfig); + return protocolSystemMessageAppendConfig is null ? throw new JsonException("Expected ProtocolSystemMessageAppendConfig value.") : new ProtocolSystemMessageConfig(protocolSystemMessageAppendConfig); + } + if (element.ValueKind == JsonValueKind.Object && (element.TryGetProperty("mode", out _) && element.GetProperty("mode").ValueKind == JsonValueKind.String && (element.GetProperty("mode").GetString() == "replace"))) + { + var protocolSystemMessageReplaceConfig = JsonSerializer.Deserialize(element, RpcJsonContext.Default.ProtocolSystemMessageReplaceConfig); + return protocolSystemMessageReplaceConfig is null ? throw new JsonException("Expected ProtocolSystemMessageReplaceConfig value.") : new ProtocolSystemMessageConfig(protocolSystemMessageReplaceConfig); + } + if (element.ValueKind == JsonValueKind.Object && (element.TryGetProperty("mode", out _) && element.GetProperty("mode").ValueKind == JsonValueKind.String && (element.GetProperty("mode").GetString() == "customize"))) + { + var protocolSystemMessageCustomizeConfig = JsonSerializer.Deserialize(element, RpcJsonContext.Default.ProtocolSystemMessageCustomizeConfig); + return protocolSystemMessageCustomizeConfig is null ? throw new JsonException("Expected ProtocolSystemMessageCustomizeConfig value.") : new ProtocolSystemMessageConfig(protocolSystemMessageCustomizeConfig); + } + + throw new JsonException("JSON value did not match any ProtocolSystemMessageConfig variant."); + } + + /// + public override void Write(Utf8JsonWriter writer, ProtocolSystemMessageConfig value, JsonSerializerOptions options) + { + if (value.ProtocolSystemMessageAppendConfig is { } protocolSystemMessageAppendConfig) + { + JsonSerializer.Serialize(writer, protocolSystemMessageAppendConfig, RpcJsonContext.Default.ProtocolSystemMessageAppendConfig); + return; + } + if (value.ProtocolSystemMessageReplaceConfig is { } protocolSystemMessageReplaceConfig) + { + JsonSerializer.Serialize(writer, protocolSystemMessageReplaceConfig, RpcJsonContext.Default.ProtocolSystemMessageReplaceConfig); + return; + } + if (value.ProtocolSystemMessageCustomizeConfig is { } protocolSystemMessageCustomizeConfig) + { + JsonSerializer.Serialize(writer, protocolSystemMessageCustomizeConfig, RpcJsonContext.Default.ProtocolSystemMessageCustomizeConfig); + return; + } + + throw new JsonException("No ProtocolSystemMessageConfig variant value is set."); + } + } +} + +/// A BYOK model definition referencing a named provider. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class ProviderModelConfig +{ + /// Optional capability overrides (vision, tool_calls, reasoning, etc.). + [JsonPropertyName("capabilities")] + public ModelCapabilitiesOverride? Capabilities { get; set; } + + /// Provider-local model id, unique within its provider. The session-wide selection id (shown in the model list and passed to switchTo) is the provider-qualified `provider/id`. + [JsonPropertyName("id")] + public string Id { get; set; } = string.Empty; + + /// Maximum context window tokens for the model. + [JsonPropertyName("maxContextWindowTokens")] + public double? MaxContextWindowTokens { get; set; } + + /// Maximum output tokens for the model. + [JsonPropertyName("maxOutputTokens")] + public double? MaxOutputTokens { get; set; } + + /// Maximum prompt/input tokens for the model. + [JsonPropertyName("maxPromptTokens")] + public double? MaxPromptTokens { get; set; } + + /// Provider-published model metadata, preserved verbatim as the public Model.metadata object. + [JsonPropertyName("metadata")] + public IDictionary? Metadata { get; set; } + + /// Well-known base model id used for behavior/capability/config lookup. Defaults to `id`. + [JsonPropertyName("modelId")] + public string? ModelId { get; set; } + + /// Display name for model pickers. Defaults to the provider-qualified selection id (`provider/id`). + [JsonPropertyName("name")] + public string? Name { get; set; } + + /// Name of the configured provider that serves this model. + [JsonPropertyName("provider")] + public string Provider { get; set; } = string.Empty; + + /// System-message configuration used when the runtime builds the standard prompt for this provider-qualified model, including general-purpose subagents. It uses the same object hierarchy as session-level systemMessage configuration, except transform actions are rejected because the current callback protocol is not model-scoped. When present, it overrides the session-wide configuration on those prompt paths. Selected custom-agent and specialized-subagent prompts remain authoritative. + [JsonPropertyName("systemMessage")] + public ProtocolSystemMessageConfig? SystemMessage { get; set; } + + /// The model name sent to the provider API for inference. Defaults to `id`. + [JsonPropertyName("wireModel")] + public string? WireModel { get; set; } +} + +/// Azure-specific provider options. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class ProviderConfigAzure +{ + /// API version. When set, uses the versioned deployment route. When omitted, uses the GA versionless v1 route. + [JsonPropertyName("apiVersion")] + public string? ApiVersion { get; set; } +} + +/// External SDK input for a named custom model provider. Ingested by the native protocol boundary before host dispatch. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class NamedProviderConfig +{ + /// Static API key used to authenticate provider requests. + [JsonPropertyName("apiKey")] + public string? ApiKey { get; set; } + + /// Azure authentication configuration for the provider. + [JsonPropertyName("azure")] + public ProviderConfigAzure? Azure { get; set; } + + /// Base URL for provider API requests. + [JsonPropertyName("baseUrl")] + public string BaseUrl { get; set; } = string.Empty; + + /// Static bearer token used to authenticate provider requests. + [JsonPropertyName("bearerToken")] + public string? BearerToken { get; set; } + + /// Whether the host supplies bearer tokens dynamically. + [JsonPropertyName("hasBearerTokenProvider")] + public bool? HasBearerTokenProvider { get; set; } + + /// Additional HTTP headers included with provider requests. + [JsonPropertyName("headers")] + public IDictionary? Headers { get; set; } + + /// The product serving the provider's models, reported in telemetry as `model_provider`. Only affects telemetry. + [JsonPropertyName("modelProvider")] + public ProviderConfigModelProvider? ModelProvider { get; set; } + + /// Unique provider name used to qualify model selection IDs. + [JsonPropertyName("name")] + public string Name { get; set; } = string.Empty; + + /// Transport used to communicate with the provider. + [JsonPropertyName("transport")] + public ProviderConfigTransport? Transport { get; set; } + + /// Provider protocol family. + [JsonPropertyName("type")] + public ProviderConfigType? Type { get; set; } + + /// Wire API used to communicate with the provider. + [JsonPropertyName("wireApi")] + public ProviderConfigWireApi? WireApi { get; set; } +} + +/// Provider configuration prepared from a discovered model. Preparing a plan changes nothing: it neither registers the model with the session nor writes durable configuration. To apply it, pass `provider` and `model` to `session.provider.add`, omitting whichever the dispositions report as already configured. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class ModelProviderConfigurationPlan +{ + /// Model definition prepared from the discovered model. Capability fields the provider did not report stay omitted rather than being asserted false. + [JsonPropertyName("model")] + public ProviderModelConfig Model { get => field ??= new(); set; } + + /// Whether `model` still needs to be registered. When `alreadyConfigured`, `selectionId` is already registered and the caller can select it without adding anything. + [JsonPropertyName("modelDisposition")] + public ModelProviderConfigurationDisposition ModelDisposition { get; set; } + + /// Provider connection prepared from the instance's inference metadata. Carries no credential; supply one if the endpoint requires it. + [JsonPropertyName("provider")] + public NamedProviderConfig Provider { get => field ??= new(); set; } + + /// Whether `provider` still needs to be registered. When `alreadyConfigured`, a provider with the same endpoint is already registered and `provider` restates it under its existing name; adding it again is rejected as a duplicate. + [JsonPropertyName("providerDisposition")] + public ModelProviderConfigurationDisposition ProviderDisposition { get; set; } + + /// Provider-qualified selection id (`provider/id`) to pass to `switchTo` once the plan is applied. + [JsonPropertyName("selectionId")] + public string SelectionId { get; set; } = string.Empty; + + /// Non-fatal warnings carried over from the discovered model, such as capabilities the provider did not report. + [JsonPropertyName("warnings")] + public IList Warnings { get => field ??= []; set; } +} + +/// A discovered instance and one of its models to translate into provider configuration. Pass back the instance and model as returned by `session.providers.discover` and `session.providers.models.list`. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +internal sealed class ModelProviderPrepareConfigurationRequest +{ + /// The discovered instance that serves the model. + [JsonPropertyName("instance")] + public ModelProviderInstance Instance { get => field ??= new(); set; } + + /// The discovered model to configure. + [JsonPropertyName("model")] + public DiscoveredModel Model { get => field ??= new(); set; } + + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; +} + /// Managed sandbox enforcement state for a session. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] public sealed class SandboxEnforcementStatus @@ -11660,6 +12712,10 @@ public sealed class CurrentModel [JsonPropertyName("pendingAutoTier")] public AutoTier? PendingAutoTier { get; set; } + /// Captured base model to restore when leaving plan mode. Omitted outside plan mode or when no plan override has captured a base model. Persistent agent model requirements apply to this model rather than the temporary plan model. + [JsonPropertyName("planBaseModelId")] + public string? PlanBaseModelId { get; set; } + /// Reasoning effort level currently applied to the active model, when one is set. Reads `Session.getReasoningEffort()` synchronously after `getSelectedModel()` resolves so the two values are reported as a snapshot. [JsonPropertyName("reasoningEffort")] public string? ReasoningEffort { get; set; } @@ -11736,78 +12792,6 @@ public sealed class ModelSwitchToResult public string? Warning { get; set; } } -/// Vision-specific limits. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class ModelCapabilitiesOverrideLimitsVision -{ - /// Maximum image size in bytes. - [JsonPropertyName("max_prompt_image_size")] - public long? MaxPromptImageSize { get; set; } - - /// Maximum number of images per prompt. - [JsonPropertyName("max_prompt_images")] - public long? MaxPromptImages { get; set; } - - /// MIME types the model accepts. - [JsonPropertyName("supported_media_types")] - public IList? SupportedMediaTypes { get; set; } -} - -/// Token limits for prompts, outputs, and context window. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class ModelCapabilitiesOverrideLimits -{ - /// Maximum total context window size in tokens. - [JsonPropertyName("max_context_window_tokens")] - public long? MaxContextWindowTokens { get; set; } - - /// Maximum number of output/completion tokens. - [JsonPropertyName("max_output_tokens")] - public long? MaxOutputTokens { get; set; } - - /// Maximum number of prompt/input tokens. - [JsonPropertyName("max_prompt_tokens")] - public long? MaxPromptTokens { get; set; } - - /// Vision-specific limits. - [JsonPropertyName("vision")] - public ModelCapabilitiesOverrideLimitsVision? Vision { get; set; } -} - -/// Feature flags indicating what the model supports. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class ModelCapabilitiesOverrideSupports -{ - /// Resolved Anthropic adaptive-thinking capability — unsupported / optional / required / adaptive_only. 'required' models reject thinking.type='enabled' with HTTP 400 but still accept 'disabled' (e.g. opus-4.7/4.8/5, sonnet-5); 'adaptive_only' models accept nothing but 'adaptive' (e.g. fable, mythos). - [JsonPropertyName("adaptive_thinking")] - public AdaptiveThinkingSupport? AdaptiveThinking { get; set; } - - /// Whether this model supports reasoning effort configuration. - [JsonPropertyName("reasoningEffort")] - public bool? ReasoningEffort { get; set; } - - /// Whether this model supports canonical tool calling. - [JsonPropertyName("toolCalls")] - public bool? ToolCalls { get; set; } - - /// Whether this model supports vision/image input. - [JsonPropertyName("vision")] - public bool? Vision { get; set; } -} - -/// Optional capability overrides (vision, tool_calls, reasoning, etc.). -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class ModelCapabilitiesOverride -{ - /// Token limits for prompts, outputs, and context window. - [JsonPropertyName("limits")] - public ModelCapabilitiesOverrideLimits? Limits { get; set; } - - /// Feature flags indicating what the model supports. - [JsonPropertyName("supports")] - public ModelCapabilitiesOverrideSupports? Supports { get; set; } -} - /// Environment variables consulted while resolving model-picker settings. public sealed class ModelPickerSettingsContextEnvironment { @@ -13179,15 +14163,36 @@ internal sealed class SessionInstructionsReloadRequest public string SessionId { get; set; } = string.Empty; } -/// Diagnostics from reloading skill definitions, with warnings and errors as separate lists. +/// Result of one customization reload component. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class SkillsLoadDiagnostics +public sealed class CustomizationReloadOutcome { - /// Errors emitted while loading skills (e.g. skills that failed to load entirely). + /// Reason for a skipped component or description of a failure, when available. + [JsonPropertyName("detail")] + public string? Detail { get; set; } + + /// Whether the component reloaded, was skipped, or failed. + [JsonPropertyName("status")] + public CustomizationReloadStatus Status { get; set; } + + /// Component whose reload was attempted or skipped. + [JsonPropertyName("subsystem")] + public CustomizationReloadSubsystem Subsystem { get; set; } +} + +/// Results of reloading discovered session customizations. Inspect outcomes for reloaded, skipped, or failed subsystems; a rejection may follow partial mutation. Changes to the model-facing prompt and tools apply on the next turn. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class CustomizationsReloadResult +{ + /// Errors from any component that could not be refreshed. [JsonPropertyName("errors")] public IList Errors { get => field ??= []; set; } - /// Warnings emitted while loading skills (e.g. skills that loaded but had issues). + /// Outcome of each component in reload order; a skipped component was not configured or loaded. + [JsonPropertyName("outcomes")] + public IList Outcomes { get => field ??= []; set; } + + /// Warnings from skill discovery. [JsonPropertyName("warnings")] public IList Warnings { get => field ??= []; set; } } @@ -14449,6 +15454,19 @@ internal sealed class SkillsDisableRequest public string SessionId { get; set; } = string.Empty; } +/// Diagnostics from reloading skill definitions, with warnings and errors as separate lists. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class SkillsLoadDiagnostics +{ + /// Errors emitted while loading skills (e.g. skills that failed to load entirely). + [JsonPropertyName("errors")] + public IList Errors { get => field ??= []; set; } + + /// Warnings emitted while loading skills (e.g. skills that loaded but had issues). + [JsonPropertyName("warnings")] + public IList Warnings { get => field ??= []; set; } +} + /// Identifies the target session. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] internal sealed class SessionSkillsReloadRequest @@ -14785,6 +15803,8 @@ public sealed class McpFilteredServer [EditorBrowsable(EditorBrowsableState.Never)] #if NET5_0_OR_GREATER [Obsolete("This member is deprecated and will be removed in a future version.", DiagnosticId = "GHCP001")] +#else + [Obsolete("This member is deprecated and will be removed in a future version.")] #endif [JsonPropertyName("enterpriseName")] public string? EnterpriseName { get; set; } @@ -17314,402 +18334,6 @@ public sealed class ProviderAddResult public IList Models { get => field ??= []; set; } } -/// RPC data type for ProtocolSystemMessageAppendConfig operations. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class ProtocolSystemMessageAppendConfig -{ - /// Text appended to the standard system prompt. - [JsonPropertyName("content")] - public string? Content { get; set; } - - /// Append-mode discriminator. Omission also selects append mode. - [JsonPropertyName("mode")] - public ProtocolAppendMode? Mode { get; set; } -} - -/// RPC data type for SystemMessageBlock operations. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class SystemMessageBlock -{ - /// Whether providers with explicit prompt caching should place a cache breakpoint after this block. - [JsonPropertyName("cacheBreakpoint")] - public bool? CacheBreakpoint { get; set; } - - /// Text content for this system-message block. - [JsonPropertyName("content")] - public string Content { get; set; } = string.Empty; - - /// Whether the block is static and may be cached independently of dynamic prompt content. - [JsonPropertyName("isStatic")] - public bool? IsStatic { get; set; } -} - -/// RPC data type for ProtocolSystemMessageReplaceConfig operations. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class ProtocolSystemMessageReplaceConfig -{ - /// Complete replacement system-message text. - [JsonPropertyName("content")] - public string Content { get; set; } = string.Empty; - - /// Optional structured blocks corresponding to the replacement content. - [JsonPropertyName("contentBlocks")] - public IList? ContentBlocks { get; set; } - - /// Replace-mode discriminator. - [JsonPropertyName("mode")] - public ProtocolReplaceMode Mode { get; set; } -} - -/// RPC data type for ProtocolStaticSectionOverride operations. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class ProtocolStaticSectionOverride -{ - /// Declarative operation applied to the section. - [JsonPropertyName("action")] - public ProtocolStaticSectionAction Action { get; set; } - - /// Optional content used by replace, append, and prepend operations. - [JsonPropertyName("content")] - public string? Content { get; set; } -} - -/// Polymorphic base type discriminated by action. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -[JsonPolymorphic( - TypeDiscriminatorPropertyName = "action", - UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FallBackToBaseType)] -[JsonDerivedType(typeof(ProtocolMarkerSectionOverrideTransform), "transform")] -[JsonDerivedType(typeof(ProtocolMarkerSectionOverridePreserve), "preserve")] -public partial class ProtocolMarkerSectionOverride -{ - /// The type discriminator. - [JsonPropertyName("action")] - public virtual string Action { get; set; } = string.Empty; -} - - -/// The transform variant of . -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class ProtocolMarkerSectionOverrideTransform : ProtocolMarkerSectionOverride -{ - /// - [JsonIgnore] - public override string Action => "transform"; -} - -/// The preserve variant of . -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class ProtocolMarkerSectionOverridePreserve : ProtocolMarkerSectionOverride -{ - /// - [JsonIgnore] - public override string Action => "preserve"; -} - -/// JSON union data type for ProtocolSectionOverride. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -[JsonConverter(typeof(Converter))] -public sealed partial class ProtocolSectionOverride -{ - /// Gets the value when this instance contains . - public ProtocolStaticSectionOverride? ProtocolStaticSectionOverride { get; } - - /// Gets the value when this instance contains . - public ProtocolMarkerSectionOverride? ProtocolMarkerSectionOverride { get; } - - /// Initializes a new instance of the class from . - public ProtocolSectionOverride(ProtocolStaticSectionOverride value) - { - ArgumentNullException.ThrowIfNull(value); - ProtocolStaticSectionOverride = value; - } - - /// Converts to . - public static implicit operator ProtocolSectionOverride(ProtocolStaticSectionOverride value) => new(value); - - /// Initializes a new instance of the class from . - public ProtocolSectionOverride(ProtocolMarkerSectionOverride value) - { - ArgumentNullException.ThrowIfNull(value); - ProtocolMarkerSectionOverride = value; - } - - /// Converts to . - public static implicit operator ProtocolSectionOverride(ProtocolMarkerSectionOverride value) => new(value); - - /// Provides a for serializing instances. - [EditorBrowsable(EditorBrowsableState.Never)] - public sealed class Converter : JsonConverter - { - /// - public override ProtocolSectionOverride Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) - { - if (reader.TokenType == JsonTokenType.Null) - { - throw new JsonException("Expected JSON object for ProtocolSectionOverride."); - } - - using var document = JsonDocument.ParseValue(ref reader); - var element = document.RootElement; - if (element.ValueKind == JsonValueKind.Object && (element.TryGetProperty("action", out _) && element.GetProperty("action").ValueKind == JsonValueKind.String && (element.GetProperty("action").GetString() == "replace" || element.GetProperty("action").GetString() == "remove" || element.GetProperty("action").GetString() == "append" || element.GetProperty("action").GetString() == "prepend"))) - { - var protocolStaticSectionOverride = JsonSerializer.Deserialize(element, RpcJsonContext.Default.ProtocolStaticSectionOverride); - return protocolStaticSectionOverride is null ? throw new JsonException("Expected ProtocolStaticSectionOverride value.") : new ProtocolSectionOverride(protocolStaticSectionOverride); - } - if ((element.ValueKind == JsonValueKind.Object && (element.TryGetProperty("action", out _) && element.GetProperty("action").ValueKind == JsonValueKind.String && (element.GetProperty("action").GetString() == "transform")) || element.ValueKind == JsonValueKind.Object && (element.TryGetProperty("action", out _) && element.GetProperty("action").ValueKind == JsonValueKind.String && (element.GetProperty("action").GetString() == "preserve")))) - { - var protocolMarkerSectionOverride = JsonSerializer.Deserialize(element, RpcJsonContext.Default.ProtocolMarkerSectionOverride); - return protocolMarkerSectionOverride is null ? throw new JsonException("Expected ProtocolMarkerSectionOverride value.") : new ProtocolSectionOverride(protocolMarkerSectionOverride); - } - - throw new JsonException("JSON value did not match any ProtocolSectionOverride variant."); - } - - /// - public override void Write(Utf8JsonWriter writer, ProtocolSectionOverride value, JsonSerializerOptions options) - { - if (value.ProtocolStaticSectionOverride is { } protocolStaticSectionOverride) - { - JsonSerializer.Serialize(writer, protocolStaticSectionOverride, RpcJsonContext.Default.ProtocolStaticSectionOverride); - return; - } - if (value.ProtocolMarkerSectionOverride is { } protocolMarkerSectionOverride) - { - JsonSerializer.Serialize(writer, protocolMarkerSectionOverride, RpcJsonContext.Default.ProtocolMarkerSectionOverride); - return; - } - - throw new JsonException("No ProtocolSectionOverride variant value is set."); - } - } -} - -/// RPC data type for ProtocolSystemMessageCustomizeConfig operations. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class ProtocolSystemMessageCustomizeConfig -{ - /// Text appended after the customized sections. - [JsonPropertyName("content")] - public string? Content { get; set; } - - /// Customize-mode discriminator. - [JsonPropertyName("mode")] - public ProtocolCustomizeMode Mode { get; set; } - - /// Named standard-prompt section overrides. - [JsonPropertyName("sections")] - public IDictionary? Sections { get; set; } -} - -/// JSON union data type for ProtocolSystemMessageConfig. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -[JsonConverter(typeof(Converter))] -public sealed partial class ProtocolSystemMessageConfig -{ - /// Gets the value when this instance contains . - public ProtocolSystemMessageAppendConfig? ProtocolSystemMessageAppendConfig { get; } - - /// Gets the value when this instance contains . - public ProtocolSystemMessageReplaceConfig? ProtocolSystemMessageReplaceConfig { get; } - - /// Gets the value when this instance contains . - public ProtocolSystemMessageCustomizeConfig? ProtocolSystemMessageCustomizeConfig { get; } - - /// Initializes a new instance of the class from . - public ProtocolSystemMessageConfig(ProtocolSystemMessageAppendConfig value) - { - ArgumentNullException.ThrowIfNull(value); - ProtocolSystemMessageAppendConfig = value; - } - - /// Converts to . - public static implicit operator ProtocolSystemMessageConfig(ProtocolSystemMessageAppendConfig value) => new(value); - - /// Initializes a new instance of the class from . - public ProtocolSystemMessageConfig(ProtocolSystemMessageReplaceConfig value) - { - ArgumentNullException.ThrowIfNull(value); - ProtocolSystemMessageReplaceConfig = value; - } - - /// Converts to . - public static implicit operator ProtocolSystemMessageConfig(ProtocolSystemMessageReplaceConfig value) => new(value); - - /// Initializes a new instance of the class from . - public ProtocolSystemMessageConfig(ProtocolSystemMessageCustomizeConfig value) - { - ArgumentNullException.ThrowIfNull(value); - ProtocolSystemMessageCustomizeConfig = value; - } - - /// Converts to . - public static implicit operator ProtocolSystemMessageConfig(ProtocolSystemMessageCustomizeConfig value) => new(value); - - /// Provides a for serializing instances. - [EditorBrowsable(EditorBrowsableState.Never)] - public sealed class Converter : JsonConverter - { - /// - public override ProtocolSystemMessageConfig Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) - { - if (reader.TokenType == JsonTokenType.Null) - { - throw new JsonException("Expected JSON object for ProtocolSystemMessageConfig."); - } - - using var document = JsonDocument.ParseValue(ref reader); - var element = document.RootElement; - if (element.ValueKind == JsonValueKind.Object && (!element.TryGetProperty("mode", out _) || (element.TryGetProperty("mode", out _) && element.GetProperty("mode").ValueKind == JsonValueKind.String && (element.GetProperty("mode").GetString() == "append")))) - { - var protocolSystemMessageAppendConfig = JsonSerializer.Deserialize(element, RpcJsonContext.Default.ProtocolSystemMessageAppendConfig); - return protocolSystemMessageAppendConfig is null ? throw new JsonException("Expected ProtocolSystemMessageAppendConfig value.") : new ProtocolSystemMessageConfig(protocolSystemMessageAppendConfig); - } - if (element.ValueKind == JsonValueKind.Object && (element.TryGetProperty("mode", out _) && element.GetProperty("mode").ValueKind == JsonValueKind.String && (element.GetProperty("mode").GetString() == "replace"))) - { - var protocolSystemMessageReplaceConfig = JsonSerializer.Deserialize(element, RpcJsonContext.Default.ProtocolSystemMessageReplaceConfig); - return protocolSystemMessageReplaceConfig is null ? throw new JsonException("Expected ProtocolSystemMessageReplaceConfig value.") : new ProtocolSystemMessageConfig(protocolSystemMessageReplaceConfig); - } - if (element.ValueKind == JsonValueKind.Object && (element.TryGetProperty("mode", out _) && element.GetProperty("mode").ValueKind == JsonValueKind.String && (element.GetProperty("mode").GetString() == "customize"))) - { - var protocolSystemMessageCustomizeConfig = JsonSerializer.Deserialize(element, RpcJsonContext.Default.ProtocolSystemMessageCustomizeConfig); - return protocolSystemMessageCustomizeConfig is null ? throw new JsonException("Expected ProtocolSystemMessageCustomizeConfig value.") : new ProtocolSystemMessageConfig(protocolSystemMessageCustomizeConfig); - } - - throw new JsonException("JSON value did not match any ProtocolSystemMessageConfig variant."); - } - - /// - public override void Write(Utf8JsonWriter writer, ProtocolSystemMessageConfig value, JsonSerializerOptions options) - { - if (value.ProtocolSystemMessageAppendConfig is { } protocolSystemMessageAppendConfig) - { - JsonSerializer.Serialize(writer, protocolSystemMessageAppendConfig, RpcJsonContext.Default.ProtocolSystemMessageAppendConfig); - return; - } - if (value.ProtocolSystemMessageReplaceConfig is { } protocolSystemMessageReplaceConfig) - { - JsonSerializer.Serialize(writer, protocolSystemMessageReplaceConfig, RpcJsonContext.Default.ProtocolSystemMessageReplaceConfig); - return; - } - if (value.ProtocolSystemMessageCustomizeConfig is { } protocolSystemMessageCustomizeConfig) - { - JsonSerializer.Serialize(writer, protocolSystemMessageCustomizeConfig, RpcJsonContext.Default.ProtocolSystemMessageCustomizeConfig); - return; - } - - throw new JsonException("No ProtocolSystemMessageConfig variant value is set."); - } - } -} - -/// A BYOK model definition referencing a named provider. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class ProviderModelConfig -{ - /// Optional capability overrides (vision, tool_calls, reasoning, etc.). - [JsonPropertyName("capabilities")] - public ModelCapabilitiesOverride? Capabilities { get; set; } - - /// Provider-local model id, unique within its provider. The session-wide selection id (shown in the model list and passed to switchTo) is the provider-qualified `provider/id`. - [JsonPropertyName("id")] - public string Id { get; set; } = string.Empty; - - /// Maximum context window tokens for the model. - [JsonPropertyName("maxContextWindowTokens")] - public double? MaxContextWindowTokens { get; set; } - - /// Maximum output tokens for the model. - [JsonPropertyName("maxOutputTokens")] - public double? MaxOutputTokens { get; set; } - - /// Maximum prompt/input tokens for the model. - [JsonPropertyName("maxPromptTokens")] - public double? MaxPromptTokens { get; set; } - - /// Provider-published model metadata, preserved verbatim as the public Model.metadata object. - [JsonPropertyName("metadata")] - public IDictionary? Metadata { get; set; } - - /// Well-known base model id used for behavior/capability/config lookup. Defaults to `id`. - [JsonPropertyName("modelId")] - public string? ModelId { get; set; } - - /// Display name for model pickers. Defaults to the provider-qualified selection id (`provider/id`). - [JsonPropertyName("name")] - public string? Name { get; set; } - - /// Name of the configured provider that serves this model. - [JsonPropertyName("provider")] - public string Provider { get; set; } = string.Empty; - - /// System-message configuration used when the runtime builds the standard prompt for this provider-qualified model, including general-purpose subagents. It uses the same object hierarchy as session-level systemMessage configuration, except transform actions are rejected because the current callback protocol is not model-scoped. When present, it overrides the session-wide configuration on those prompt paths. Selected custom-agent and specialized-subagent prompts remain authoritative. - [JsonPropertyName("systemMessage")] - public ProtocolSystemMessageConfig? SystemMessage { get; set; } - - /// The model name sent to the provider API for inference. Defaults to `id`. - [JsonPropertyName("wireModel")] - public string? WireModel { get; set; } -} - -/// Azure-specific provider options. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class ProviderConfigAzure -{ - /// API version. When set, uses the versioned deployment route. When omitted, uses the GA versionless v1 route. - [JsonPropertyName("apiVersion")] - public string? ApiVersion { get; set; } -} - -/// External SDK input for a named custom model provider. Ingested by the native protocol boundary before host dispatch. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class NamedProviderConfig -{ - /// Static API key used to authenticate provider requests. - [JsonPropertyName("apiKey")] - public string? ApiKey { get; set; } - - /// Azure authentication configuration for the provider. - [JsonPropertyName("azure")] - public ProviderConfigAzure? Azure { get; set; } - - /// Base URL for provider API requests. - [JsonPropertyName("baseUrl")] - public string BaseUrl { get; set; } = string.Empty; - - /// Static bearer token used to authenticate provider requests. - [JsonPropertyName("bearerToken")] - public string? BearerToken { get; set; } - - /// Whether the host supplies bearer tokens dynamically. - [JsonPropertyName("hasBearerTokenProvider")] - public bool? HasBearerTokenProvider { get; set; } - - /// Additional HTTP headers included with provider requests. - [JsonPropertyName("headers")] - public IDictionary? Headers { get; set; } - - /// The product serving the provider's models, reported in telemetry as `model_provider`. Only affects telemetry. - [JsonPropertyName("modelProvider")] - public ProviderConfigModelProvider? ModelProvider { get; set; } - - /// Unique provider name used to qualify model selection IDs. - [JsonPropertyName("name")] - public string Name { get; set; } = string.Empty; - - /// Transport used to communicate with the provider. - [JsonPropertyName("transport")] - public ProviderConfigTransport? Transport { get; set; } - - /// Provider protocol family. - [JsonPropertyName("type")] - public ProviderConfigType? Type { get; set; } - - /// Wire API used to communicate with the provider. - [JsonPropertyName("wireApi")] - public ProviderConfigWireApi? WireApi { get; set; } -} - /// BYOK providers and/or models to add to the session's registry at runtime. Both fields are optional; provide providers, models, or both. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] internal sealed class ProviderAddRequest @@ -17982,199 +18606,6 @@ public sealed class ProviderConfig public string? WireModel { get; set; } } -/// Credential-injection capability flags applied while the sandbox is enabled. For the same capability independent of sandboxing, and matched to the credential's GitHub host, see `shell.credentials`; the two are additive. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class SandboxConfigAuth -{ - /// Whether to authenticate sandboxed gh through the local masking proxy. The child receives a fake GH_TOKEN; its real value is substituted only at github.com, api.github.com and uploads.github.com (github.com because gh repo clone authenticates git through gh auth git-credential). The repository's GitHub account takes precedence over the Copilot login. Default: false (opt-in). - [JsonPropertyName("gh")] - public bool? Gh { get; set; } - - /// Whether to authenticate sandboxed HTTPS git through the local masking proxy. The child receives a fake `http.<url>.extraheader`; the real Authorization header is substituted only at its original HTTPS host, port, and repository path scope. github.com uses the Copilot token; other forges use credentials resolved from the user's own helper on the host. Default: false (opt-in). - [JsonPropertyName("git")] - public bool? Git { get; set; } -} - -/// Destinations authorized to receive one masked environment credential. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class SandboxMaskedEnvVar -{ - /// Nonempty list of HTTPS injection hostnames or *.example.com patterns. Bare * is not accepted. These grants never override the sandbox network policy. Values in plaintext HTTP requests, URLs, bodies, encoded credentials, and signed requests are not substituted. - [JsonPropertyName("injectHosts")] - public IList InjectHosts { get => field ??= []; set; } -} - -/// Whole-value environment credential masking for sandboxed children. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class SandboxCredentialsConfig -{ - /// Environment variable names and their HTTPS injection destinations. Absent variables stay absent. No real values or sentinels are stored in this map. - [JsonPropertyName("envVars")] - public IDictionary EnvVars { get => field ??= new Dictionary(); set; } -} - -/// macOS seatbelt experimental options. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class SandboxConfigUserPolicyExperimentalSeatbelt -{ - /// Whether the macOS seatbelt profile may access the keychain. - [JsonPropertyName("keychainAccess")] - public bool? KeychainAccess { get; set; } -} - -/// Platform-specific experimental policy fields. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class SandboxConfigUserPolicyExperimental -{ - /// macOS seatbelt experimental options. - [JsonPropertyName("seatbelt")] - public SandboxConfigUserPolicyExperimentalSeatbelt? Seatbelt { get; set; } -} - -/// Filesystem rules to merge into the base policy. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class SandboxConfigUserPolicyFilesystem -{ - /// Whether to clear the policy when the session exits. - [JsonPropertyName("clearPolicyOnExit")] - public bool? ClearPolicyOnExit { get; set; } - - /// Paths explicitly denied. - [JsonPropertyName("deniedPaths")] - public IList? DeniedPaths { get; set; } - - /// Paths granted read-only access. - [JsonPropertyName("readonlyPaths")] - public IList? ReadonlyPaths { get; set; } - - /// Paths granted read/write access. - [JsonPropertyName("readwritePaths")] - public IList? ReadwritePaths { get; set; } -} - -/// HTTP proxy configuration for sandboxed traffic. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class SandboxConfigUserPolicyNetworkProxy -{ - /// Optional password for proxy authentication, combined with the URL at spawn time. The persisted value may be a literal password, a `${secret:…}` reference resolved from the OS keychain, or a `${VAR}`/`$VAR` environment reference; it is resolved just before the sandboxed process routes through the proxy. The /sandbox dialog stores a real password in the OS keychain and persists only a `${secret:…}` placeholder (never plaintext in settings.json); the field is masked in the dialog and redacted by /settings show. - [JsonPropertyName("password")] - public string? Password { get; set; } - - /// Proxy URL (e.g. http://proxy.example.com:8080). The port is optional and defaults to the scheme's standard port when omitted; an explicit port must be between 1 and 65535. Credentials must not be embedded here — a `user:pass@` authority is rejected; put them in the separate `username`/`password` fields. A credential-free http:// loopback proxy URL is routed through the localhost proxy automatically; loopback covers localhost and any *.localhost subdomain, the whole 127.0.0.0/8 range, ::1, and IPv4-mapped loopback (::ffff:127.0.0.1). An https:// URL, or one with a username/password set, is used as-is. - [JsonPropertyName("url")] - public string Url { get; set; } = string.Empty; - - /// Optional username for proxy authentication. Combined with the URL (and `password`) into `user:pass@host` when the sandboxed process routes through the proxy. - [JsonPropertyName("username")] - public string? Username { get; set; } -} - -/// Network rules to merge into the base policy. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class SandboxConfigUserPolicyNetwork -{ - /// Hosts allowed through the built-in sandbox proxy. A non-empty list denies unmatched hosts; an absent or empty list allows all hosts not blocked. Supports exact hostnames, IP addresses, and *.example.com for strict subdomains. Host rules do not override the outbound or local-network toggles. - [JsonPropertyName("allowedHosts")] - public IList? AllowedHosts { get; set; } - - /// Whether traffic to local/loopback addresses is allowed. - [JsonPropertyName("allowLocalNetwork")] - public bool? AllowLocalNetwork { get; set; } - - /// Whether outbound network traffic is allowed at all. - [JsonPropertyName("allowOutbound")] - public bool? AllowOutbound { get; set; } - - /// Hosts denied by the built-in sandbox proxy. Deny rules take precedence over allowedHosts. A domain also denies all its subdomains. IP addresses match exactly; *.example.com matches strict subdomains, and * denies every host. - [JsonPropertyName("blockedHosts")] - public IList? BlockedHosts { get; set; } - - /// HTTP(S) proxy for sandboxed traffic. This is the built-in local proxy's upstream: every sandboxed command reaches it through a loopback listener, so credentials stay in the runtime and never reach the child. On Windows the sandbox also needs local network access, because it reaches that listener over host loopback. Configure credentials in the separate username/password fields. The transient local listener URL is never persisted. - [JsonPropertyName("proxy")] - public SandboxConfigUserPolicyNetworkProxy? Proxy { get; set; } -} - -/// macOS seatbelt-specific options. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class SandboxConfigUserPolicySeatbelt -{ - /// Whether the macOS seatbelt profile may access the keychain. - [JsonPropertyName("keychainAccess")] - public bool? KeychainAccess { get; set; } -} - -/// User-managed sandbox policy fragment merged into the auto-discovered base policy. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class SandboxConfigUserPolicy -{ - /// Deprecated legacy location for `seatbelt`; read only when the top-level `seatbelt` is absent. - [JsonPropertyName("experimental")] - public SandboxConfigUserPolicyExperimental? Experimental { get; set; } - - /// Filesystem rules to merge into the base policy. - [JsonPropertyName("filesystem")] - public SandboxConfigUserPolicyFilesystem? Filesystem { get; set; } - - /// Network rules to merge into the base policy. - [JsonPropertyName("network")] - public SandboxConfigUserPolicyNetwork? Network { get; set; } - - /// macOS seatbelt options to merge into the base policy. - [JsonPropertyName("seatbelt")] - public SandboxConfigUserPolicySeatbelt? Seatbelt { get; set; } -} - -/// Resolved sandbox configuration. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class SandboxConfig -{ - /// Whether to auto-add the current working directory to readwritePaths. Default: true. - [JsonPropertyName("addCurrentWorkingDirectory")] - public bool? AddCurrentWorkingDirectory { get; set; } - - /// Whether the agent may request that an individual command run outside the sandbox, which the host then approves or denies through the usual permission flow. A host capability flag rather than part of the policy: it is stripped from the effective spawn policy and only has an effect while `enabled` is true. Fail-closed, unlike the opt-out flags on this object: omitting it offers no bypass. Default: false (opt-in). - [JsonPropertyName("allowBypass")] - public bool? AllowBypass { get; set; } - - /// Whether to auto-grant read access to tool directories discovered on PATH and in toolchain environment variables (GOROOT, JAVA_HOME, VIRTUAL_ENV, and similar), and to common developer-tool caches, config, and toolchains. Writable grants cover scratch caches, the Unix GitHub CLI cache, and Cargo's registry, git store, and lock/tracker files. A relocated CARGO_HOME gets the same narrow split: registry and git are read-write; bin is read-only; the home root, config.toml, and credentials.toml stay ungranted. Set to false to disable every grant listed above; user-installed toolchains and caches then need explicit userPolicy.filesystem readonlyPaths and readwritePaths entries. The working directory (see addCurrentWorkingDirectory), temporary storage, session log paths, and system locations follow their own rules and stay granted. Default: true (enabled by default; set to false to opt out). - [JsonPropertyName("allowDevToolAccess")] - public bool? AllowDevToolAccess { get; set; } - - /// Credential-injection capability flags. - [JsonPropertyName("auth")] - public SandboxConfigAuth? Auth { get; set; } - - /// Opt-in whole-value environment masking for sandboxed shell, MCP, and LSP children. Configured names get random sentinels; the local proxy substitutes them only in HTTPS request headers at their injection hosts. Approved bypasses skip masking and the sandbox proxy, so bypassed shells may receive the real environment values. Disabled or explicitly opted-out routes are not protected. No credential values are stored in this configuration. - [JsonPropertyName("credentials")] - public SandboxCredentialsConfig? Credentials { get; set; } - - /// Whether sandboxing is enabled for the session. - [JsonPropertyName("enabled")] - public bool Enabled { get; set; } - - /// The `sandboxLspServers` counterpart of `managedMcpRoutingLocked`. - [JsonInclude] - [JsonPropertyName("managedLspRoutingLocked")] - internal bool? ManagedLspRoutingLocked { get; set; } - - /// Set by the runtime when a managed policy forced `sandboxMcpServers` on and took the local opt-out away. Provenance rather than policy: it lets a sandbox startup failure point at the administrator instead of a setting the next managed merge would override, and it is ignored when comparing two configs for change. Only the managed merge may set it; a caller-supplied value is stripped. - [JsonInclude] - [JsonPropertyName("managedMcpRoutingLocked")] - internal bool? ManagedMcpRoutingLocked { get; set; } - - /// Whether language servers the session launches are confined by the sandbox. Only an explicit `false` opts out. Ignored while `enabled` is false. Default: true (enabled by default; set to false to opt out). - [JsonPropertyName("sandboxLspServers")] - public bool? SandboxLspServers { get; set; } - - /// Whether MCP servers the session launches are confined by the sandbox. Only an explicit `false` opts out; doing so also lets remote-MCP egress leave the sandbox, so the flag and `enabled` are always read together. Ignored while `enabled` is false. Default: true (enabled by default; set to false to opt out). - [JsonPropertyName("sandboxMcpServers")] - public bool? SandboxMcpServers { get; set; } - - /// User-managed sandbox policy fragment merged into the auto-discovered base policy. - [JsonPropertyName("userPolicy")] - public SandboxConfigUserPolicy? UserPolicy { get; set; } -} - /// /// Command-scoped GitHub credential injection for the shell commands an agent runs. /// @@ -18484,6 +18915,8 @@ internal sealed class SessionUpdateOptionsParams [EditorBrowsable(EditorBrowsableState.Never)] #if NET5_0_OR_GREATER [Obsolete("This member is deprecated and will be removed in a future version.", DiagnosticId = "GHCP001")] +#else + [Obsolete("This member is deprecated and will be removed in a future version.")] #endif [JsonPropertyName("shellInitProfile")] public string? ShellInitProfile { get; set; } @@ -25072,6 +25505,10 @@ public sealed class SessionFsError /// Free-form detail about the error, for logging/diagnostics. [JsonPropertyName("message")] public string? Message { get; set; } + + /// For failed writeFile requests only: true if the provider changed the target before failing. Omit when unknown or unchanged. + [JsonPropertyName("writeChanged")] + public bool? WriteChanged { get; set; } } /// File content as a UTF-8 string, or a filesystem error if the read failed. @@ -25100,6 +25537,32 @@ public sealed class SessionFsReadFileRequest public string SessionId { get; set; } = string.Empty; } +/// File bytes as standard base64, or a filesystem error if the read failed. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class SessionFsReadFileBytesResult +{ + /// Exact file bytes encoded as standard base64. + [JsonPropertyName("content")] + public string Content { get; set; } = string.Empty; + + /// Describes a filesystem error. + [JsonPropertyName("error")] + public SessionFsError? Error { get; set; } +} + +/// Path of the binary file to read from the client-provided session filesystem. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class SessionFsReadFileBytesRequest +{ + /// Path using SessionFs conventions. + [JsonPropertyName("path")] + public string Path { get; set; } = string.Empty; + + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; +} + /// File path, content to write, and optional mode for the client-provided session filesystem. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] public sealed class SessionFsWriteFileRequest @@ -25121,6 +25584,27 @@ public sealed class SessionFsWriteFileRequest public string SessionId { get; set; } = string.Empty; } +/// File path, standard-base64-encoded bytes to write, and optional mode for the client-provided session filesystem. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class SessionFsWriteFileBytesRequest +{ + /// Exact file bytes encoded as standard base64. + [JsonPropertyName("content")] + public string Content { get; set; } = string.Empty; + + /// Optional POSIX-style mode for newly created files. + [JsonPropertyName("mode")] + public long? Mode { get; set; } + + /// Path using SessionFs conventions. + [JsonPropertyName("path")] + public string Path { get; set; } = string.Empty; + + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; +} + /// File path, content to append, and optional mode for the client-provided session filesystem. Implementations create parent directories as needed. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] public sealed class SessionFsAppendFileRequest @@ -26821,6 +27305,75 @@ public override void Write(Utf8JsonWriter writer, ModelProviderKind value, JsonS } +/// State of the persistent certificate authority of the sandbox credential proxy. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +[JsonConverter(typeof(Converter))] +[DebuggerDisplay("{Value,nq}")] +public readonly struct SandboxProxyCaState : IEquatable +{ + private readonly string? _value; + + /// Initializes a new instance of the struct. + /// The value to associate with this . + [JsonConstructor] + public SandboxProxyCaState(string value) + { + ArgumentException.ThrowIfNullOrWhiteSpace(value); + _value = value; + } + + /// Gets the value associated with this . + public string Value => _value ?? string.Empty; + + /// This platform has no supported OS trust store. The proxy uses a per-process certificate bundle. + public static SandboxProxyCaState Unsupported { get; } = new("unsupported"); + + /// OS trust does not include the certificate authority, or none is stored. + public static SandboxProxyCaState NotInstalled { get; } = new("notInstalled"); + + /// OS trust includes the stored certificate authority. + public static SandboxProxyCaState Installed { get; } = new("installed"); + + /// The runtime could not read the certificate authority or the OS trust store. + public static SandboxProxyCaState Error { get; } = new("error"); + + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(SandboxProxyCaState left, SandboxProxyCaState right) => left.Equals(right); + + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(SandboxProxyCaState left, SandboxProxyCaState right) => !(left == right); + + /// + public override bool Equals(object? obj) => obj is SandboxProxyCaState other && Equals(other); + + /// + public bool Equals(SandboxProxyCaState other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + + /// + public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); + + /// + public override string ToString() => Value; + + /// Provides a for serializing instances. + [EditorBrowsable(EditorBrowsableState.Never)] + public sealed class Converter : JsonConverter + { + /// + public override SandboxProxyCaState Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); + } + + /// + public override void Write(Utf8JsonWriter writer, SandboxProxyCaState value, JsonSerializerOptions options) + { + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(SandboxProxyCaState)); + } + } +} + + /// Server transport type: stdio, http, sse (deprecated), or memory. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] @@ -32028,26 +32581,428 @@ public AgentRegistrySpawnValidationErrorField(string value) /// The session name parameter. public static AgentRegistrySpawnValidationErrorField Name { get; } = new("name"); - /// The agentName parameter. - public static AgentRegistrySpawnValidationErrorField AgentName { get; } = new("agentName"); + /// The agentName parameter. + public static AgentRegistrySpawnValidationErrorField AgentName { get; } = new("agentName"); + + /// The model parameter. + public static AgentRegistrySpawnValidationErrorField Model { get; } = new("model"); + + /// The permissionMode parameter. + public static AgentRegistrySpawnValidationErrorField PermissionMode { get; } = new("permissionMode"); + + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(AgentRegistrySpawnValidationErrorField left, AgentRegistrySpawnValidationErrorField right) => left.Equals(right); + + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(AgentRegistrySpawnValidationErrorField left, AgentRegistrySpawnValidationErrorField right) => !(left == right); + + /// + public override bool Equals(object? obj) => obj is AgentRegistrySpawnValidationErrorField other && Equals(other); + + /// + public bool Equals(AgentRegistrySpawnValidationErrorField other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + + /// + public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); + + /// + public override string ToString() => Value; + + /// Provides a for serializing instances. + [EditorBrowsable(EditorBrowsableState.Never)] + public sealed class Converter : JsonConverter + { + /// + public override AgentRegistrySpawnValidationErrorField Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); + } + + /// + public override void Write(Utf8JsonWriter writer, AgentRegistrySpawnValidationErrorField value, JsonSerializerOptions options) + { + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(AgentRegistrySpawnValidationErrorField)); + } + } +} + + +/// Categorized reason for the rejection. Low-cardinality enum so telemetry can aggregate by reason without leaking raw paths or agent/model names. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +[JsonConverter(typeof(Converter))] +[DebuggerDisplay("{Value,nq}")] +public readonly struct AgentRegistrySpawnValidationErrorReason : IEquatable +{ + private readonly string? _value; + + /// Initializes a new instance of the struct. + /// The value to associate with this . + [JsonConstructor] + public AgentRegistrySpawnValidationErrorReason(string value) + { + ArgumentException.ThrowIfNullOrWhiteSpace(value); + _value = value; + } + + /// Gets the value associated with this . + public string Value => _value ?? string.Empty; + + /// Provided cwd does not exist on disk. + public static AgentRegistrySpawnValidationErrorReason CwdNotFound { get; } = new("cwd-not-found"); + + /// Provided cwd exists but is not a directory. + public static AgentRegistrySpawnValidationErrorReason CwdNotDirectory { get; } = new("cwd-not-directory"); + + /// Session name failed validateSessionName. + public static AgentRegistrySpawnValidationErrorReason InvalidName { get; } = new("invalid-name"); + + /// Requested agent name was not found in builtin or custom agents. + public static AgentRegistrySpawnValidationErrorReason UnknownAgent { get; } = new("unknown-agent"); + + /// Requested model is not available to this session. + public static AgentRegistrySpawnValidationErrorReason UnknownModel { get; } = new("unknown-model"); + + /// Caller asked for permissionMode='yolo' but the controller is not currently in allow-all mode. + public static AgentRegistrySpawnValidationErrorReason YoloNotAllowed { get; } = new("yolo-not-allowed"); + + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(AgentRegistrySpawnValidationErrorReason left, AgentRegistrySpawnValidationErrorReason right) => left.Equals(right); + + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(AgentRegistrySpawnValidationErrorReason left, AgentRegistrySpawnValidationErrorReason right) => !(left == right); + + /// + public override bool Equals(object? obj) => obj is AgentRegistrySpawnValidationErrorReason other && Equals(other); + + /// + public bool Equals(AgentRegistrySpawnValidationErrorReason other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + + /// + public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); + + /// + public override string ToString() => Value; + + /// Provides a for serializing instances. + [EditorBrowsable(EditorBrowsableState.Never)] + public sealed class Converter : JsonConverter + { + /// + public override AgentRegistrySpawnValidationErrorReason Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); + } + + /// + public override void Write(Utf8JsonWriter writer, AgentRegistrySpawnValidationErrorReason value, JsonSerializerOptions options) + { + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(AgentRegistrySpawnValidationErrorReason)); + } + } +} + + +/// Permission posture for the new session. 'yolo' requires the controller-local session to currently be in allow-all mode. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +[JsonConverter(typeof(Converter))] +[DebuggerDisplay("{Value,nq}")] +public readonly struct AgentRegistrySpawnPermissionMode : IEquatable +{ + private readonly string? _value; + + /// Initializes a new instance of the struct. + /// The value to associate with this . + [JsonConstructor] + public AgentRegistrySpawnPermissionMode(string value) + { + ArgumentException.ThrowIfNullOrWhiteSpace(value); + _value = value; + } + + /// Gets the value associated with this . + public string Value => _value ?? string.Empty; + + /// Standard permission posture (prompts for each request). + public static AgentRegistrySpawnPermissionMode Default { get; } = new("default"); + + /// Full allow-all (requires the controller-local session to currently be in allow-all mode). + public static AgentRegistrySpawnPermissionMode Yolo { get; } = new("yolo"); + + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(AgentRegistrySpawnPermissionMode left, AgentRegistrySpawnPermissionMode right) => left.Equals(right); + + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(AgentRegistrySpawnPermissionMode left, AgentRegistrySpawnPermissionMode right) => !(left == right); + + /// + public override bool Equals(object? obj) => obj is AgentRegistrySpawnPermissionMode other && Equals(other); + + /// + public bool Equals(AgentRegistrySpawnPermissionMode other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + + /// + public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); + + /// + public override string ToString() => Value; + + /// Provides a for serializing instances. + [EditorBrowsable(EditorBrowsableState.Never)] + public sealed class Converter : JsonConverter + { + /// + public override AgentRegistrySpawnPermissionMode Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); + } + + /// + public override void Write(Utf8JsonWriter writer, AgentRegistrySpawnPermissionMode value, JsonSerializerOptions options) + { + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(AgentRegistrySpawnPermissionMode)); + } + } +} + + +/// The UI mode the agent was in when this message was sent. Defaults to the session's current mode. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +[JsonConverter(typeof(Converter))] +[DebuggerDisplay("{Value,nq}")] +public readonly struct SendAgentMode : IEquatable +{ + private readonly string? _value; + + /// Initializes a new instance of the struct. + /// The value to associate with this . + [JsonConstructor] + public SendAgentMode(string value) + { + ArgumentException.ThrowIfNullOrWhiteSpace(value); + _value = value; + } + + /// Gets the value associated with this . + public string Value => _value ?? string.Empty; + + /// The agent is responding interactively to the user. + public static SendAgentMode Interactive { get; } = new("interactive"); + + /// The agent is preparing a plan before making changes. + public static SendAgentMode Plan { get; } = new("plan"); + + /// The agent is working autonomously toward task completion. + public static SendAgentMode Autopilot { get; } = new("autopilot"); + + /// The agent is in shell-focused UI mode. + public static SendAgentMode Shell { get; } = new("shell"); + + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(SendAgentMode left, SendAgentMode right) => left.Equals(right); + + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(SendAgentMode left, SendAgentMode right) => !(left == right); + + /// + public override bool Equals(object? obj) => obj is SendAgentMode other && Equals(other); + + /// + public bool Equals(SendAgentMode other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + + /// + public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); + + /// + public override string ToString() => Value; + + /// Provides a for serializing instances. + [EditorBrowsable(EditorBrowsableState.Never)] + public sealed class Converter : JsonConverter + { + /// + public override SendAgentMode Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); + } + + /// + public override void Write(Utf8JsonWriter writer, SendAgentMode value, JsonSerializerOptions options) + { + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(SendAgentMode)); + } + } +} + + +/// How to deliver the message. `enqueue` (default) appends to the message queue. `immediate` interjects during an in-progress turn. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +[JsonConverter(typeof(Converter))] +[DebuggerDisplay("{Value,nq}")] +public readonly struct SendMode : IEquatable +{ + private readonly string? _value; + + /// Initializes a new instance of the struct. + /// The value to associate with this . + [JsonConstructor] + public SendMode(string value) + { + ArgumentException.ThrowIfNullOrWhiteSpace(value); + _value = value; + } + + /// Gets the value associated with this . + public string Value => _value ?? string.Empty; + + /// Append the message to the normal session queue. + public static SendMode Enqueue { get; } = new("enqueue"); + + /// Interject the message during the in-progress turn. + public static SendMode Immediate { get; } = new("immediate"); + + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(SendMode left, SendMode right) => left.Equals(right); + + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(SendMode left, SendMode right) => !(left == right); + + /// + public override bool Equals(object? obj) => obj is SendMode other && Equals(other); + + /// + public bool Equals(SendMode other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + + /// + public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); + + /// + public override string ToString() => Value; + + /// Provides a for serializing instances. + [EditorBrowsable(EditorBrowsableState.Never)] + public sealed class Converter : JsonConverter + { + /// + public override SendMode Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); + } + + /// + public override void Write(Utf8JsonWriter writer, SendMode value, JsonSerializerOptions options) + { + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(SendMode)); + } + } +} + + +/// Log severity level. Determines how the message is displayed in the timeline. Defaults to "info". +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +[JsonConverter(typeof(Converter))] +[DebuggerDisplay("{Value,nq}")] +public readonly struct SessionLogLevel : IEquatable +{ + private readonly string? _value; + + /// Initializes a new instance of the struct. + /// The value to associate with this . + [JsonConstructor] + public SessionLogLevel(string value) + { + ArgumentException.ThrowIfNullOrWhiteSpace(value); + _value = value; + } + + /// Gets the value associated with this . + public string Value => _value ?? string.Empty; + + /// Informational message. + public static SessionLogLevel Info { get; } = new("info"); + + /// Warning message that may require attention. + public static SessionLogLevel Warning { get; } = new("warning"); + + /// Error message describing a failure. + public static SessionLogLevel Error { get; } = new("error"); + + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(SessionLogLevel left, SessionLogLevel right) => left.Equals(right); + + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(SessionLogLevel left, SessionLogLevel right) => !(left == right); + + /// + public override bool Equals(object? obj) => obj is SessionLogLevel other && Equals(other); + + /// + public bool Equals(SessionLogLevel other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + + /// + public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); + + /// + public override string ToString() => Value; + + /// Provides a for serializing instances. + [EditorBrowsable(EditorBrowsableState.Never)] + public sealed class Converter : JsonConverter + { + /// + public override SessionLogLevel Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); + } + + /// + public override void Write(Utf8JsonWriter writer, SessionLogLevel value, JsonSerializerOptions options) + { + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(SessionLogLevel)); + } + } +} + + +/// When the runtime may run an adapter without an explicit user action. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +[JsonConverter(typeof(Converter))] +[DebuggerDisplay("{Value,nq}")] +public readonly struct ModelProviderAutomaticDiscoveryMode : IEquatable +{ + private readonly string? _value; + + /// Initializes a new instance of the struct. + /// The value to associate with this . + [JsonConstructor] + public ModelProviderAutomaticDiscoveryMode(string value) + { + ArgumentException.ThrowIfNullOrWhiteSpace(value); + _value = value; + } + + /// Gets the value associated with this . + public string Value => _value ?? string.Empty; + + /// The adapter declares that automatic discovery is safe when the other policy fields are satisfied. + public static ModelProviderAutomaticDiscoveryMode Automatic { get; } = new("automatic"); - /// The model parameter. - public static AgentRegistrySpawnValidationErrorField Model { get; } = new("model"); + /// The adapter may refresh instances the user already configured, but must not scan for new instances automatically. + public static ModelProviderAutomaticDiscoveryMode ConfiguredOnly { get; } = new("configuredOnly"); - /// The permissionMode parameter. - public static AgentRegistrySpawnValidationErrorField PermissionMode { get; } = new("permissionMode"); + /// The adapter must run only after an explicit user action. + public static ModelProviderAutomaticDiscoveryMode Explicit { get; } = new("explicit"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(AgentRegistrySpawnValidationErrorField left, AgentRegistrySpawnValidationErrorField right) => left.Equals(right); + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(ModelProviderAutomaticDiscoveryMode left, ModelProviderAutomaticDiscoveryMode right) => left.Equals(right); - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(AgentRegistrySpawnValidationErrorField left, AgentRegistrySpawnValidationErrorField right) => !(left == right); + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(ModelProviderAutomaticDiscoveryMode left, ModelProviderAutomaticDiscoveryMode right) => !(left == right); /// - public override bool Equals(object? obj) => obj is AgentRegistrySpawnValidationErrorField other && Equals(other); + public override bool Equals(object? obj) => obj is ModelProviderAutomaticDiscoveryMode other && Equals(other); /// - public bool Equals(AgentRegistrySpawnValidationErrorField other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + public bool Equals(ModelProviderAutomaticDiscoveryMode other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); /// public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); @@ -32055,74 +33010,71 @@ public AgentRegistrySpawnValidationErrorField(string value) /// public override string ToString() => Value; - /// Provides a for serializing instances. + /// Provides a for serializing instances. [EditorBrowsable(EditorBrowsableState.Never)] - public sealed class Converter : JsonConverter + public sealed class Converter : JsonConverter { /// - public override AgentRegistrySpawnValidationErrorField Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override ModelProviderAutomaticDiscoveryMode Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); } /// - public override void Write(Utf8JsonWriter writer, AgentRegistrySpawnValidationErrorField value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, ModelProviderAutomaticDiscoveryMode value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(AgentRegistrySpawnValidationErrorField)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(ModelProviderAutomaticDiscoveryMode)); } } } -/// Categorized reason for the rejection. Low-cardinality enum so telemetry can aggregate by reason without leaking raw paths or agent/model names. +/// Network reach an adapter may use during discovery. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct AgentRegistrySpawnValidationErrorReason : IEquatable +public readonly struct ModelProviderDiscoveryNetworkScope : IEquatable { private readonly string? _value; - /// Initializes a new instance of the struct. - /// The value to associate with this . + /// Initializes a new instance of the struct. + /// The value to associate with this . [JsonConstructor] - public AgentRegistrySpawnValidationErrorReason(string value) + public ModelProviderDiscoveryNetworkScope(string value) { ArgumentException.ThrowIfNullOrWhiteSpace(value); _value = value; } - /// Gets the value associated with this . + /// Gets the value associated with this . public string Value => _value ?? string.Empty; - /// Provided cwd does not exist on disk. - public static AgentRegistrySpawnValidationErrorReason CwdNotFound { get; } = new("cwd-not-found"); - - /// Provided cwd exists but is not a directory. - public static AgentRegistrySpawnValidationErrorReason CwdNotDirectory { get; } = new("cwd-not-directory"); + /// Discovery does not contact a network service. + public static ModelProviderDiscoveryNetworkScope None { get; } = new("none"); - /// Session name failed validateSessionName. - public static AgentRegistrySpawnValidationErrorReason InvalidName { get; } = new("invalid-name"); + /// Discovery is limited to loopback addresses on the local machine. + public static ModelProviderDiscoveryNetworkScope LoopbackOnly { get; } = new("loopbackOnly"); - /// Requested agent name was not found in builtin or custom agents. - public static AgentRegistrySpawnValidationErrorReason UnknownAgent { get; } = new("unknown-agent"); + /// Discovery contacts only endpoints the user already configured. + public static ModelProviderDiscoveryNetworkScope ConfiguredEndpointOnly { get; } = new("configuredEndpointOnly"); - /// Requested model is not available to this session. - public static AgentRegistrySpawnValidationErrorReason UnknownModel { get; } = new("unknown-model"); + /// Discovery may scan or contact the local network. + public static ModelProviderDiscoveryNetworkScope LocalNetwork { get; } = new("localNetwork"); - /// Caller asked for permissionMode='yolo' but the controller is not currently in allow-all mode. - public static AgentRegistrySpawnValidationErrorReason YoloNotAllowed { get; } = new("yolo-not-allowed"); + /// Discovery may contact remote internet services. + public static ModelProviderDiscoveryNetworkScope Internet { get; } = new("internet"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(AgentRegistrySpawnValidationErrorReason left, AgentRegistrySpawnValidationErrorReason right) => left.Equals(right); + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(ModelProviderDiscoveryNetworkScope left, ModelProviderDiscoveryNetworkScope right) => left.Equals(right); - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(AgentRegistrySpawnValidationErrorReason left, AgentRegistrySpawnValidationErrorReason right) => !(left == right); + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(ModelProviderDiscoveryNetworkScope left, ModelProviderDiscoveryNetworkScope right) => !(left == right); /// - public override bool Equals(object? obj) => obj is AgentRegistrySpawnValidationErrorReason other && Equals(other); + public override bool Equals(object? obj) => obj is ModelProviderDiscoveryNetworkScope other && Equals(other); /// - public bool Equals(AgentRegistrySpawnValidationErrorReason other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + public bool Equals(ModelProviderDiscoveryNetworkScope other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); /// public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); @@ -32130,62 +33082,68 @@ public AgentRegistrySpawnValidationErrorReason(string value) /// public override string ToString() => Value; - /// Provides a for serializing instances. + /// Provides a for serializing instances. [EditorBrowsable(EditorBrowsableState.Never)] - public sealed class Converter : JsonConverter + public sealed class Converter : JsonConverter { /// - public override AgentRegistrySpawnValidationErrorReason Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override ModelProviderDiscoveryNetworkScope Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); } /// - public override void Write(Utf8JsonWriter writer, AgentRegistrySpawnValidationErrorReason value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, ModelProviderDiscoveryNetworkScope value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(AgentRegistrySpawnValidationErrorReason)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(ModelProviderDiscoveryNetworkScope)); } } } -/// Permission posture for the new session. 'yolo' requires the controller-local session to currently be in allow-all mode. +/// Kind of component that supplied a provider adapter or row. Attribution does not confer authority. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct AgentRegistrySpawnPermissionMode : IEquatable +public readonly struct ModelProviderProvenanceSource : IEquatable { private readonly string? _value; - /// Initializes a new instance of the struct. - /// The value to associate with this . + /// Initializes a new instance of the struct. + /// The value to associate with this . [JsonConstructor] - public AgentRegistrySpawnPermissionMode(string value) + public ModelProviderProvenanceSource(string value) { ArgumentException.ThrowIfNullOrWhiteSpace(value); _value = value; } - /// Gets the value associated with this . + /// Gets the value associated with this . public string Value => _value ?? string.Empty; - /// Standard permission posture (prompts for each request). - public static AgentRegistrySpawnPermissionMode Default { get; } = new("default"); + /// Built into the runtime. + public static ModelProviderProvenanceSource BuiltIn { get; } = new("builtIn"); - /// Full allow-all (requires the controller-local session to currently be in allow-all mode). - public static AgentRegistrySpawnPermissionMode Yolo { get; } = new("yolo"); + /// Derived from existing user configuration. + public static ModelProviderProvenanceSource Configured { get; } = new("configured"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(AgentRegistrySpawnPermissionMode left, AgentRegistrySpawnPermissionMode right) => left.Equals(right); + /// Supplied by an extension. + public static ModelProviderProvenanceSource Extension { get; } = new("extension"); - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(AgentRegistrySpawnPermissionMode left, AgentRegistrySpawnPermissionMode right) => !(left == right); + /// Supplied by another trusted contributor. + public static ModelProviderProvenanceSource Custom { get; } = new("custom"); + + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(ModelProviderProvenanceSource left, ModelProviderProvenanceSource right) => left.Equals(right); + + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(ModelProviderProvenanceSource left, ModelProviderProvenanceSource right) => !(left == right); /// - public override bool Equals(object? obj) => obj is AgentRegistrySpawnPermissionMode other && Equals(other); + public override bool Equals(object? obj) => obj is ModelProviderProvenanceSource other && Equals(other); /// - public bool Equals(AgentRegistrySpawnPermissionMode other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + public bool Equals(ModelProviderProvenanceSource other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); /// public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); @@ -32193,65 +33151,62 @@ public AgentRegistrySpawnPermissionMode(string value) /// public override string ToString() => Value; - /// Provides a for serializing instances. + /// Provides a for serializing instances. [EditorBrowsable(EditorBrowsableState.Never)] - public sealed class Converter : JsonConverter + public sealed class Converter : JsonConverter { /// - public override AgentRegistrySpawnPermissionMode Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override ModelProviderProvenanceSource Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); } /// - public override void Write(Utf8JsonWriter writer, AgentRegistrySpawnPermissionMode value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, ModelProviderProvenanceSource value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(AgentRegistrySpawnPermissionMode)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(ModelProviderProvenanceSource)); } } } -/// How far OneAuth may go to acquire the requested token. +/// Transport to be used for provider requests. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct EntraTokenInteraction : IEquatable +public readonly struct ProviderEndpointTransport : IEquatable { private readonly string? _value; - /// Initializes a new instance of the struct. - /// The value to associate with this . + /// Initializes a new instance of the struct. + /// The value to associate with this . [JsonConstructor] - public EntraTokenInteraction(string value) + public ProviderEndpointTransport(string value) { ArgumentException.ThrowIfNullOrWhiteSpace(value); _value = value; } - /// Gets the value associated with this . + /// Gets the value associated with this . public string Value => _value ?? string.Empty; - /// Acquire the token without any user interaction, failing if interaction would be required. - public static EntraTokenInteraction Silent { get; } = new("silent"); - - /// Allow interactive acquisition, prompting the user only when a cached or silent token is unavailable. - public static EntraTokenInteraction Interactive { get; } = new("interactive"); + /// HTTP request/streaming transport. + public static ProviderEndpointTransport Http { get; } = new("http"); - /// Always prompt interactively, bypassing any cached or silently-refreshable token. - public static EntraTokenInteraction ForceInteractive { get; } = new("force-interactive"); + /// WebSocket transport. + public static ProviderEndpointTransport Websockets { get; } = new("websockets"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(EntraTokenInteraction left, EntraTokenInteraction right) => left.Equals(right); + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(ProviderEndpointTransport left, ProviderEndpointTransport right) => left.Equals(right); - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(EntraTokenInteraction left, EntraTokenInteraction right) => !(left == right); + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(ProviderEndpointTransport left, ProviderEndpointTransport right) => !(left == right); /// - public override bool Equals(object? obj) => obj is EntraTokenInteraction other && Equals(other); + public override bool Equals(object? obj) => obj is ProviderEndpointTransport other && Equals(other); /// - public bool Equals(EntraTokenInteraction other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + public bool Equals(ProviderEndpointTransport other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); /// public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); @@ -32259,68 +33214,65 @@ public EntraTokenInteraction(string value) /// public override string ToString() => Value; - /// Provides a for serializing instances. + /// Provides a for serializing instances. [EditorBrowsable(EditorBrowsableState.Never)] - public sealed class Converter : JsonConverter + public sealed class Converter : JsonConverter { /// - public override EntraTokenInteraction Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override ProviderEndpointTransport Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); } /// - public override void Write(Utf8JsonWriter writer, EntraTokenInteraction value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, ProviderEndpointTransport value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(EntraTokenInteraction)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(ProviderEndpointTransport)); } } } -/// The UI mode the agent was in when this message was sent. Defaults to the session's current mode. +/// Provider family. Matches the `type` field of a BYOK provider config. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct SendAgentMode : IEquatable +public readonly struct ProviderEndpointType : IEquatable { private readonly string? _value; - /// Initializes a new instance of the struct. - /// The value to associate with this . + /// Initializes a new instance of the struct. + /// The value to associate with this . [JsonConstructor] - public SendAgentMode(string value) + public ProviderEndpointType(string value) { ArgumentException.ThrowIfNullOrWhiteSpace(value); _value = value; } - /// Gets the value associated with this . + /// Gets the value associated with this . public string Value => _value ?? string.Empty; - /// The agent is responding interactively to the user. - public static SendAgentMode Interactive { get; } = new("interactive"); - - /// The agent is preparing a plan before making changes. - public static SendAgentMode Plan { get; } = new("plan"); + /// OpenAI-compatible endpoint (use the OpenAI client library). + public static ProviderEndpointType Openai { get; } = new("openai"); - /// The agent is working autonomously toward task completion. - public static SendAgentMode Autopilot { get; } = new("autopilot"); + /// Azure OpenAI endpoint (use the OpenAI client library with the Azure base URL). + public static ProviderEndpointType Azure { get; } = new("azure"); - /// The agent is in shell-focused UI mode. - public static SendAgentMode Shell { get; } = new("shell"); + /// Anthropic endpoint (use the Anthropic client library). + public static ProviderEndpointType Anthropic { get; } = new("anthropic"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(SendAgentMode left, SendAgentMode right) => left.Equals(right); + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(ProviderEndpointType left, ProviderEndpointType right) => left.Equals(right); - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(SendAgentMode left, SendAgentMode right) => !(left == right); + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(ProviderEndpointType left, ProviderEndpointType right) => !(left == right); /// - public override bool Equals(object? obj) => obj is SendAgentMode other && Equals(other); + public override bool Equals(object? obj) => obj is ProviderEndpointType other && Equals(other); /// - public bool Equals(SendAgentMode other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + public bool Equals(ProviderEndpointType other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); /// public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); @@ -32328,62 +33280,62 @@ public SendAgentMode(string value) /// public override string ToString() => Value; - /// Provides a for serializing instances. + /// Provides a for serializing instances. [EditorBrowsable(EditorBrowsableState.Never)] - public sealed class Converter : JsonConverter + public sealed class Converter : JsonConverter { /// - public override SendAgentMode Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override ProviderEndpointType Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); } /// - public override void Write(Utf8JsonWriter writer, SendAgentMode value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, ProviderEndpointType value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(SendAgentMode)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(ProviderEndpointType)); } } } -/// How to deliver the message. `enqueue` (default) appends to the message queue. `immediate` interjects during an in-progress turn. +/// Wire API to be used, when required for the provider type. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct SendMode : IEquatable +public readonly struct ProviderEndpointWireApi : IEquatable { private readonly string? _value; - /// Initializes a new instance of the struct. - /// The value to associate with this . + /// Initializes a new instance of the struct. + /// The value to associate with this . [JsonConstructor] - public SendMode(string value) + public ProviderEndpointWireApi(string value) { ArgumentException.ThrowIfNullOrWhiteSpace(value); _value = value; } - /// Gets the value associated with this . + /// Gets the value associated with this . public string Value => _value ?? string.Empty; - /// Append the message to the normal session queue. - public static SendMode Enqueue { get; } = new("enqueue"); + /// Classic chat-completions request shape. + public static ProviderEndpointWireApi Completions { get; } = new("completions"); - /// Interject the message during the in-progress turn. - public static SendMode Immediate { get; } = new("immediate"); + /// Newer responses request shape. + public static ProviderEndpointWireApi Responses { get; } = new("responses"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(SendMode left, SendMode right) => left.Equals(right); + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(ProviderEndpointWireApi left, ProviderEndpointWireApi right) => left.Equals(right); - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(SendMode left, SendMode right) => !(left == right); + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(ProviderEndpointWireApi left, ProviderEndpointWireApi right) => !(left == right); /// - public override bool Equals(object? obj) => obj is SendMode other && Equals(other); + public override bool Equals(object? obj) => obj is ProviderEndpointWireApi other && Equals(other); /// - public bool Equals(SendMode other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + public bool Equals(ProviderEndpointWireApi other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); /// public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); @@ -32391,65 +33343,68 @@ public SendMode(string value) /// public override string ToString() => Value; - /// Provides a for serializing instances. + /// Provides a for serializing instances. [EditorBrowsable(EditorBrowsableState.Never)] - public sealed class Converter : JsonConverter + public sealed class Converter : JsonConverter { /// - public override SendMode Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override ProviderEndpointWireApi Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); } /// - public override void Write(Utf8JsonWriter writer, SendMode value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, ProviderEndpointWireApi value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(SendMode)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(ProviderEndpointWireApi)); } } } -/// Log severity level. Determines how the message is displayed in the timeline. Defaults to "info". +/// Typed outcome for a provider operation. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct SessionLogLevel : IEquatable +public readonly struct ModelProviderOperationOutcomeCode : IEquatable { private readonly string? _value; - /// Initializes a new instance of the struct. - /// The value to associate with this . + /// Initializes a new instance of the struct. + /// The value to associate with this . [JsonConstructor] - public SessionLogLevel(string value) + public ModelProviderOperationOutcomeCode(string value) { ArgumentException.ThrowIfNullOrWhiteSpace(value); _value = value; } - /// Gets the value associated with this . + /// Gets the value associated with this . public string Value => _value ?? string.Empty; - /// Informational message. - public static SessionLogLevel Info { get; } = new("info"); + /// The operation completed successfully; an empty inventory is valid. + public static ModelProviderOperationOutcomeCode Success { get; } = new("success"); - /// Warning message that may require attention. - public static SessionLogLevel Warning { get; } = new("warning"); + /// The provider or instance is absent during discovery, status, or model listing. Distinct from a successful empty inventory. + public static ModelProviderOperationOutcomeCode Absent { get; } = new("absent"); - /// Error message describing a failure. - public static SessionLogLevel Error { get; } = new("error"); + /// The provider is configured or expected but could not be reached. + public static ModelProviderOperationOutcomeCode Unreachable { get; } = new("unreachable"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(SessionLogLevel left, SessionLogLevel right) => left.Equals(right); + /// The operation failed for a reason other than absence or reachability. + public static ModelProviderOperationOutcomeCode Failed { get; } = new("failed"); - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(SessionLogLevel left, SessionLogLevel right) => !(left == right); + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(ModelProviderOperationOutcomeCode left, ModelProviderOperationOutcomeCode right) => left.Equals(right); + + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(ModelProviderOperationOutcomeCode left, ModelProviderOperationOutcomeCode right) => !(left == right); /// - public override bool Equals(object? obj) => obj is SessionLogLevel other && Equals(other); + public override bool Equals(object? obj) => obj is ModelProviderOperationOutcomeCode other && Equals(other); /// - public bool Equals(SessionLogLevel other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + public bool Equals(ModelProviderOperationOutcomeCode other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); /// public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); @@ -32457,65 +33412,59 @@ public SessionLogLevel(string value) /// public override string ToString() => Value; - /// Provides a for serializing instances. + /// Provides a for serializing instances. [EditorBrowsable(EditorBrowsableState.Never)] - public sealed class Converter : JsonConverter + public sealed class Converter : JsonConverter { /// - public override SessionLogLevel Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override ModelProviderOperationOutcomeCode Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); } /// - public override void Write(Utf8JsonWriter writer, SessionLogLevel value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, ModelProviderOperationOutcomeCode value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(SessionLogLevel)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(ModelProviderOperationOutcomeCode)); } } } -/// Disposition of a permission request as observed by the responding client. +/// Defines the allowed values. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct PermissionDecisionOutcome : IEquatable +public readonly struct ProtocolAppendMode : IEquatable { private readonly string? _value; - /// Initializes a new instance of the struct. - /// The value to associate with this . + /// Initializes a new instance of the struct. + /// The value to associate with this . [JsonConstructor] - public PermissionDecisionOutcome(string value) + public ProtocolAppendMode(string value) { ArgumentException.ThrowIfNullOrWhiteSpace(value); _value = value; } - /// Gets the value associated with this . + /// Gets the value associated with this . public string Value => _value ?? string.Empty; - /// The request was approved automatically without a new human decision. - public static PermissionDecisionOutcome AutoApproved { get; } = new("auto_approved"); - - /// The request was denied without an interactive user decision; source records why. - public static PermissionDecisionOutcome AutopilotDenied { get; } = new("autopilot_denied"); - - /// The response came from an interactive user prompt. - public static PermissionDecisionOutcome PromptedUser { get; } = new("prompted_user"); + /// Gets the append value. + public static ProtocolAppendMode Append { get; } = new("append"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(PermissionDecisionOutcome left, PermissionDecisionOutcome right) => left.Equals(right); + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(ProtocolAppendMode left, ProtocolAppendMode right) => left.Equals(right); - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(PermissionDecisionOutcome left, PermissionDecisionOutcome right) => !(left == right); + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(ProtocolAppendMode left, ProtocolAppendMode right) => !(left == right); /// - public override bool Equals(object? obj) => obj is PermissionDecisionOutcome other && Equals(other); + public override bool Equals(object? obj) => obj is ProtocolAppendMode other && Equals(other); /// - public bool Equals(PermissionDecisionOutcome other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + public bool Equals(ProtocolAppendMode other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); /// public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); @@ -32523,65 +33472,59 @@ public PermissionDecisionOutcome(string value) /// public override string ToString() => Value; - /// Provides a for serializing instances. + /// Provides a for serializing instances. [EditorBrowsable(EditorBrowsableState.Never)] - public sealed class Converter : JsonConverter + public sealed class Converter : JsonConverter { /// - public override PermissionDecisionOutcome Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override ProtocolAppendMode Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); } /// - public override void Write(Utf8JsonWriter writer, PermissionDecisionOutcome value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, ProtocolAppendMode value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(PermissionDecisionOutcome)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(ProtocolAppendMode)); } } } -/// Response capability available to the client when it settled a permission request. +/// Defines the allowed values. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct PermissionResponseCapability : IEquatable +public readonly struct ProtocolReplaceMode : IEquatable { private readonly string? _value; - /// Initializes a new instance of the struct. - /// The value to associate with this . + /// Initializes a new instance of the struct. + /// The value to associate with this . [JsonConstructor] - public PermissionResponseCapability(string value) + public ProtocolReplaceMode(string value) { ArgumentException.ThrowIfNullOrWhiteSpace(value); _value = value; } - /// Gets the value associated with this . + /// Gets the value associated with this . public string Value => _value ?? string.Empty; - /// The client could ask a user for this decision. - public static PermissionResponseCapability Interactive { get; } = new("interactive"); - - /// The client could return an automated response but could not ask a user. - public static PermissionResponseCapability Headless { get; } = new("headless"); - - /// The client had no response path available. - public static PermissionResponseCapability None { get; } = new("none"); + /// Gets the replace value. + public static ProtocolReplaceMode Replace { get; } = new("replace"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(PermissionResponseCapability left, PermissionResponseCapability right) => left.Equals(right); + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(ProtocolReplaceMode left, ProtocolReplaceMode right) => left.Equals(right); - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(PermissionResponseCapability left, PermissionResponseCapability right) => !(left == right); + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(ProtocolReplaceMode left, ProtocolReplaceMode right) => !(left == right); /// - public override bool Equals(object? obj) => obj is PermissionResponseCapability other && Equals(other); + public override bool Equals(object? obj) => obj is ProtocolReplaceMode other && Equals(other); /// - public bool Equals(PermissionResponseCapability other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + public bool Equals(ProtocolReplaceMode other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); /// public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); @@ -32589,71 +33532,59 @@ public PermissionResponseCapability(string value) /// public override string ToString() => Value; - /// Provides a for serializing instances. + /// Provides a for serializing instances. [EditorBrowsable(EditorBrowsableState.Never)] - public sealed class Converter : JsonConverter + public sealed class Converter : JsonConverter { /// - public override PermissionResponseCapability Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override ProtocolReplaceMode Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); } /// - public override void Write(Utf8JsonWriter writer, PermissionResponseCapability value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, ProtocolReplaceMode value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(PermissionResponseCapability)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(ProtocolReplaceMode)); } } } -/// Client surface that submitted a permission response. +/// Defines the allowed values. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct PermissionDecisionSurface : IEquatable +public readonly struct ProtocolCustomizeMode : IEquatable { private readonly string? _value; - /// Initializes a new instance of the struct. - /// The value to associate with this . + /// Initializes a new instance of the struct. + /// The value to associate with this . [JsonConstructor] - public PermissionDecisionSurface(string value) + public ProtocolCustomizeMode(string value) { ArgumentException.ThrowIfNullOrWhiteSpace(value); _value = value; } - /// Gets the value associated with this . + /// Gets the value associated with this . public string Value => _value ?? string.Empty; - /// The interactive Copilot CLI terminal UI. - public static PermissionDecisionSurface Tui { get; } = new("tui"); - - /// The non-interactive Copilot CLI prompt mode. - public static PermissionDecisionSurface PromptMode { get; } = new("prompt_mode"); - - /// The Copilot App client. - public static PermissionDecisionSurface CopilotApp { get; } = new("copilot_app"); - - /// An Agent Client Protocol host. - public static PermissionDecisionSurface Acp { get; } = new("acp"); - - /// A generic Copilot SDK client. - public static PermissionDecisionSurface Sdk { get; } = new("sdk"); + /// Gets the customize value. + public static ProtocolCustomizeMode Customize { get; } = new("customize"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(PermissionDecisionSurface left, PermissionDecisionSurface right) => left.Equals(right); + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(ProtocolCustomizeMode left, ProtocolCustomizeMode right) => left.Equals(right); - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(PermissionDecisionSurface left, PermissionDecisionSurface right) => !(left == right); + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(ProtocolCustomizeMode left, ProtocolCustomizeMode right) => !(left == right); /// - public override bool Equals(object? obj) => obj is PermissionDecisionSurface other && Equals(other); + public override bool Equals(object? obj) => obj is ProtocolCustomizeMode other && Equals(other); /// - public bool Equals(PermissionDecisionSurface other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + public bool Equals(ProtocolCustomizeMode other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); /// public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); @@ -32661,80 +33592,68 @@ public PermissionDecisionSurface(string value) /// public override string ToString() => Value; - /// Provides a for serializing instances. + /// Provides a for serializing instances. [EditorBrowsable(EditorBrowsableState.Never)] - public sealed class Converter : JsonConverter + public sealed class Converter : JsonConverter { /// - public override PermissionDecisionSurface Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override ProtocolCustomizeMode Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); } /// - public override void Write(Utf8JsonWriter writer, PermissionDecisionSurface value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, ProtocolCustomizeMode value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(PermissionDecisionSurface)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(ProtocolCustomizeMode)); } } } -/// Authentication type. +/// Defines the allowed values. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct AuthInfoType : IEquatable +public readonly struct ProtocolStaticSectionAction : IEquatable { private readonly string? _value; - /// Initializes a new instance of the struct. - /// The value to associate with this . + /// Initializes a new instance of the struct. + /// The value to associate with this . [JsonConstructor] - public AuthInfoType(string value) + public ProtocolStaticSectionAction(string value) { ArgumentException.ThrowIfNullOrWhiteSpace(value); _value = value; } - /// Gets the value associated with this . + /// Gets the value associated with this . public string Value => _value ?? string.Empty; - /// Authentication provided by a GitHub App HMAC credential. - public static AuthInfoType Hmac { get; } = new("hmac"); - - /// Authentication resolved from environment-provided credentials. - public static AuthInfoType Env { get; } = new("env"); - - /// Authentication from an interactive user sign-in. - public static AuthInfoType User { get; } = new("user"); - - /// Authentication delegated to the GitHub CLI. - public static AuthInfoType GhCli { get; } = new("gh-cli"); - - /// Authentication from an API key credential. - public static AuthInfoType ApiKey { get; } = new("api-key"); + /// Replace the section content. + public static ProtocolStaticSectionAction Replace { get; } = new("replace"); - /// Authentication from a GitHub token. - public static AuthInfoType Token { get; } = new("token"); + /// Remove the section content. + public static ProtocolStaticSectionAction Remove { get; } = new("remove"); - /// Authentication from an SDK GitHub token callback. - public static AuthInfoType TokenProvider { get; } = new("token-provider"); + /// Append content to the section. + public static ProtocolStaticSectionAction Append { get; } = new("append"); - /// Authentication from a Copilot API token. - public static AuthInfoType CopilotApiToken { get; } = new("copilot-api-token"); + /// Prepend content to the section. + public static ProtocolStaticSectionAction Prepend { get; } = new("prepend"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(AuthInfoType left, AuthInfoType right) => left.Equals(right); + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(ProtocolStaticSectionAction left, ProtocolStaticSectionAction right) => left.Equals(right); - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(AuthInfoType left, AuthInfoType right) => !(left == right); + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(ProtocolStaticSectionAction left, ProtocolStaticSectionAction right) => !(left == right); /// - public override bool Equals(object? obj) => obj is AuthInfoType other && Equals(other); + public override bool Equals(object? obj) => obj is ProtocolStaticSectionAction other && Equals(other); /// - public bool Equals(AuthInfoType other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + public bool Equals(ProtocolStaticSectionAction other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); /// public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); @@ -32742,71 +33661,62 @@ public AuthInfoType(string value) /// public override string ToString() => Value; - /// Provides a for serializing instances. + /// Provides a for serializing instances. [EditorBrowsable(EditorBrowsableState.Never)] - public sealed class Converter : JsonConverter + public sealed class Converter : JsonConverter { /// - public override AuthInfoType Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override ProtocolStaticSectionAction Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); } /// - public override void Write(Utf8JsonWriter writer, AuthInfoType value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, ProtocolStaticSectionAction value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(AuthInfoType)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(ProtocolStaticSectionAction)); } } } -/// The provider kind stamped on a signed-in account. +/// Whether a planned configuration entry is new or already present in the session registry. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct AccountKind : IEquatable +public readonly struct ModelProviderConfigurationDisposition : IEquatable { private readonly string? _value; - /// Initializes a new instance of the struct. - /// The value to associate with this . + /// Initializes a new instance of the struct. + /// The value to associate with this . [JsonConstructor] - public AccountKind(string value) + public ModelProviderConfigurationDisposition(string value) { ArgumentException.ThrowIfNullOrWhiteSpace(value); _value = value; } - /// Gets the value associated with this . + /// Gets the value associated with this . public string Value => _value ?? string.Empty; - /// An OAuth github.com account. - public static AccountKind GitHubDotCom { get; } = new("githubDotCom"); - - /// A GitHub Enterprise Cloud account — a GitHub account on a non-github.com host, e.g. *.ghe.com. - public static AccountKind Proxima { get; } = new("proxima"); - - /// A GitHub (EMU) account derived from a base Entra identity. - public static AccountKind EntraEmu { get; } = new("entraEmu"); - - /// A base Microsoft Entra identity. - public static AccountKind Entra { get; } = new("entra"); + /// No matching entry is registered; the caller should add the entry. + public static ModelProviderConfigurationDisposition Create { get; } = new("create"); - /// A Microsoft 365 Copilot (Loki) inference account derived from the same base Entra identity as an EMU account; its bearer is a Loki-scoped inference token consumed through the model-provider path, not the GitHub switcher. - public static AccountKind Loki { get; } = new("loki"); + /// An equivalent entry is already registered; the caller should reuse it rather than adding a duplicate. + public static ModelProviderConfigurationDisposition AlreadyConfigured { get; } = new("alreadyConfigured"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(AccountKind left, AccountKind right) => left.Equals(right); + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(ModelProviderConfigurationDisposition left, ModelProviderConfigurationDisposition right) => left.Equals(right); - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(AccountKind left, AccountKind right) => !(left == right); + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(ModelProviderConfigurationDisposition left, ModelProviderConfigurationDisposition right) => !(left == right); /// - public override bool Equals(object? obj) => obj is AccountKind other && Equals(other); + public override bool Equals(object? obj) => obj is ModelProviderConfigurationDisposition other && Equals(other); /// - public bool Equals(AccountKind other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + public bool Equals(ModelProviderConfigurationDisposition other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); /// public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); @@ -32814,65 +33724,77 @@ public AccountKind(string value) /// public override string ToString() => Value; - /// Provides a for serializing instances. + /// Provides a for serializing instances. [EditorBrowsable(EditorBrowsableState.Never)] - public sealed class Converter : JsonConverter + public sealed class Converter : JsonConverter { /// - public override AccountKind Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override ModelProviderConfigurationDisposition Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); } /// - public override void Write(Utf8JsonWriter writer, AccountKind value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, ModelProviderConfigurationDisposition value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(AccountKind)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(ModelProviderConfigurationDisposition)); } } } -/// A provider a consumer may interactively sign in with. +/// The product serving the model, reported in telemetry as `model_provider`. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct LoginProviderKind : IEquatable +public readonly struct ProviderConfigModelProvider : IEquatable { private readonly string? _value; - /// Initializes a new instance of the struct. - /// The value to associate with this . + /// Initializes a new instance of the struct. + /// The value to associate with this . [JsonConstructor] - public LoginProviderKind(string value) + public ProviderConfigModelProvider(string value) { ArgumentException.ThrowIfNullOrWhiteSpace(value); _value = value; } - /// Gets the value associated with this . + /// Gets the value associated with this . public string Value => _value ?? string.Empty; - /// OAuth github.com sign-in via the browser (web loopback + PKCE). - public static LoginProviderKind GitHubDotCom { get; } = new("githubDotCom"); + /// OpenAI API. + public static ProviderConfigModelProvider Openai { get; } = new("openai"); - /// A GitHub Enterprise Cloud account — a GitHub account on a non-github.com host, e.g. *.ghe.com; the host is supplied interactively through the neutral input-required step. - public static LoginProviderKind Proxima { get; } = new("proxima"); + /// Anthropic API. + public static ProviderConfigModelProvider Anthropic { get; } = new("anthropic"); - /// Microsoft Entra sign-in that derives a GitHub (EMU) credential. - public static LoginProviderKind Entra { get; } = new("entra"); + /// Azure OpenAI Service. + public static ProviderConfigModelProvider AzureOpenai { get; } = new("azure_openai"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(LoginProviderKind left, LoginProviderKind right) => left.Equals(right); + /// Ollama. + public static ProviderConfigModelProvider Ollama { get; } = new("ollama"); - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(LoginProviderKind left, LoginProviderKind right) => !(left == right); + /// LM Studio. + public static ProviderConfigModelProvider LmStudio { get; } = new("lm_studio"); + + /// Foundry Local. + public static ProviderConfigModelProvider FoundryLocal { get; } = new("foundry_local"); + + /// llama.cpp server. + public static ProviderConfigModelProvider LlamaCpp { get; } = new("llama_cpp"); + + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(ProviderConfigModelProvider left, ProviderConfigModelProvider right) => left.Equals(right); + + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(ProviderConfigModelProvider left, ProviderConfigModelProvider right) => !(left == right); /// - public override bool Equals(object? obj) => obj is LoginProviderKind other && Equals(other); + public override bool Equals(object? obj) => obj is ProviderConfigModelProvider other && Equals(other); /// - public bool Equals(LoginProviderKind other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + public bool Equals(ProviderConfigModelProvider other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); /// public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); @@ -32880,65 +33802,62 @@ public LoginProviderKind(string value) /// public override string ToString() => Value; - /// Provides a for serializing instances. + /// Provides a for serializing instances. [EditorBrowsable(EditorBrowsableState.Never)] - public sealed class Converter : JsonConverter + public sealed class Converter : JsonConverter { /// - public override LoginProviderKind Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override ProviderConfigModelProvider Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); } /// - public override void Write(Utf8JsonWriter writer, LoginProviderKind value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, ProviderConfigModelProvider value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(LoginProviderKind)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(ProviderConfigModelProvider)); } } } -/// Terminal disposition of a login persistence attempt. +/// Provider transport. Defaults to "http". [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct AuthLoginResultStatus : IEquatable +public readonly struct ProviderConfigTransport : IEquatable { private readonly string? _value; - /// Initializes a new instance of the struct. - /// The value to associate with this . + /// Initializes a new instance of the struct. + /// The value to associate with this . [JsonConstructor] - public AuthLoginResultStatus(string value) + public ProviderConfigTransport(string value) { ArgumentException.ThrowIfNullOrWhiteSpace(value); _value = value; } - /// Gets the value associated with this . + /// Gets the value associated with this . public string Value => _value ?? string.Empty; - /// The credential was persisted and the account is signed in. - public static AuthLoginResultStatus Completed { get; } = new("completed"); - - /// Persistence needs explicit consent to store the token in plaintext. - public static AuthLoginResultStatus NeedsPlaintextConsent { get; } = new("needs-plaintext-consent"); + /// HTTP request/streaming transport. + public static ProviderConfigTransport Http { get; } = new("http"); - /// The user declined plaintext persistence. - public static AuthLoginResultStatus Declined { get; } = new("declined"); + /// WebSocket transport. + public static ProviderConfigTransport Websockets { get; } = new("websockets"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(AuthLoginResultStatus left, AuthLoginResultStatus right) => left.Equals(right); + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(ProviderConfigTransport left, ProviderConfigTransport right) => left.Equals(right); - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(AuthLoginResultStatus left, AuthLoginResultStatus right) => !(left == right); + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(ProviderConfigTransport left, ProviderConfigTransport right) => !(left == right); /// - public override bool Equals(object? obj) => obj is AuthLoginResultStatus other && Equals(other); + public override bool Equals(object? obj) => obj is ProviderConfigTransport other && Equals(other); /// - public bool Equals(AuthLoginResultStatus other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + public bool Equals(ProviderConfigTransport other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); /// public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); @@ -32946,68 +33865,65 @@ public AuthLoginResultStatus(string value) /// public override string ToString() => Value; - /// Provides a for serializing instances. + /// Provides a for serializing instances. [EditorBrowsable(EditorBrowsableState.Never)] - public sealed class Converter : JsonConverter + public sealed class Converter : JsonConverter { /// - public override AuthLoginResultStatus Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override ProviderConfigTransport Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); } /// - public override void Write(Utf8JsonWriter writer, AuthLoginResultStatus value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, ProviderConfigTransport value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(AuthLoginResultStatus)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(ProviderConfigTransport)); } } } -/// Source category for a collected debug bundle entry. +/// Provider type. Defaults to "openai" for generic OpenAI-compatible APIs. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct DebugCollectLogsSource : IEquatable +public readonly struct ProviderConfigType : IEquatable { private readonly string? _value; - /// Initializes a new instance of the struct. - /// The value to associate with this . + /// Initializes a new instance of the struct. + /// The value to associate with this . [JsonConstructor] - public DebugCollectLogsSource(string value) + public ProviderConfigType(string value) { ArgumentException.ThrowIfNullOrWhiteSpace(value); _value = value; } - /// Gets the value associated with this . + /// Gets the value associated with this . public string Value => _value ?? string.Empty; - /// Session event log. - public static DebugCollectLogsSource Events { get; } = new("events"); - - /// Process log for the session. - public static DebugCollectLogsSource ProcessLog { get; } = new("process-log"); + /// Generic OpenAI-compatible API. + public static ProviderConfigType Openai { get; } = new("openai"); - /// Interactive shell log for the session. - public static DebugCollectLogsSource ShellLog { get; } = new("shell-log"); + /// Azure OpenAI Service endpoint. + public static ProviderConfigType Azure { get; } = new("azure"); - /// Caller-provided diagnostic entry. - public static DebugCollectLogsSource Additional { get; } = new("additional"); + /// Anthropic API endpoint. + public static ProviderConfigType Anthropic { get; } = new("anthropic"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(DebugCollectLogsSource left, DebugCollectLogsSource right) => left.Equals(right); + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(ProviderConfigType left, ProviderConfigType right) => left.Equals(right); - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(DebugCollectLogsSource left, DebugCollectLogsSource right) => !(left == right); + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(ProviderConfigType left, ProviderConfigType right) => !(left == right); /// - public override bool Equals(object? obj) => obj is DebugCollectLogsSource other && Equals(other); + public override bool Equals(object? obj) => obj is ProviderConfigType other && Equals(other); /// - public bool Equals(DebugCollectLogsSource other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + public bool Equals(ProviderConfigType other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); /// public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); @@ -33015,62 +33931,62 @@ public DebugCollectLogsSource(string value) /// public override string ToString() => Value; - /// Provides a for serializing instances. + /// Provides a for serializing instances. [EditorBrowsable(EditorBrowsableState.Never)] - public sealed class Converter : JsonConverter + public sealed class Converter : JsonConverter { /// - public override DebugCollectLogsSource Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override ProviderConfigType Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); } /// - public override void Write(Utf8JsonWriter writer, DebugCollectLogsSource value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, ProviderConfigType value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(DebugCollectLogsSource)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(ProviderConfigType)); } } } -/// Destination kind that was written. +/// Wire API format (openai/azure only). Defaults to "completions". [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct DebugCollectLogsResultKind : IEquatable +public readonly struct ProviderConfigWireApi : IEquatable { private readonly string? _value; - /// Initializes a new instance of the struct. - /// The value to associate with this . + /// Initializes a new instance of the struct. + /// The value to associate with this . [JsonConstructor] - public DebugCollectLogsResultKind(string value) + public ProviderConfigWireApi(string value) { ArgumentException.ThrowIfNullOrWhiteSpace(value); _value = value; } - /// Gets the value associated with this . + /// Gets the value associated with this . public string Value => _value ?? string.Empty; - /// A .tgz archive was written. - public static DebugCollectLogsResultKind Archive { get; } = new("archive"); + /// OpenAI Chat Completions wire format. + public static ProviderConfigWireApi Completions { get; } = new("completions"); - /// A directory containing the collected files was written. - public static DebugCollectLogsResultKind Directory { get; } = new("directory"); + /// OpenAI Responses API wire format. + public static ProviderConfigWireApi Responses { get; } = new("responses"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(DebugCollectLogsResultKind left, DebugCollectLogsResultKind right) => left.Equals(right); + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(ProviderConfigWireApi left, ProviderConfigWireApi right) => left.Equals(right); - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(DebugCollectLogsResultKind left, DebugCollectLogsResultKind right) => !(left == right); + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(ProviderConfigWireApi left, ProviderConfigWireApi right) => !(left == right); /// - public override bool Equals(object? obj) => obj is DebugCollectLogsResultKind other && Equals(other); + public override bool Equals(object? obj) => obj is ProviderConfigWireApi other && Equals(other); /// - public bool Equals(DebugCollectLogsResultKind other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + public bool Equals(ProviderConfigWireApi other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); /// public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); @@ -33078,62 +33994,65 @@ public DebugCollectLogsResultKind(string value) /// public override string ToString() => Value; - /// Provides a for serializing instances. + /// Provides a for serializing instances. [EditorBrowsable(EditorBrowsableState.Never)] - public sealed class Converter : JsonConverter + public sealed class Converter : JsonConverter { /// - public override DebugCollectLogsResultKind Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override ProviderConfigWireApi Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); } /// - public override void Write(Utf8JsonWriter writer, DebugCollectLogsResultKind value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, ProviderConfigWireApi value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(DebugCollectLogsResultKind)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(ProviderConfigWireApi)); } } } -/// Kind of caller-provided debug log entry. +/// Disposition of a permission request as observed by the responding client. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct DebugCollectLogsEntryKind : IEquatable +public readonly struct PermissionDecisionOutcome : IEquatable { private readonly string? _value; - /// Initializes a new instance of the struct. - /// The value to associate with this . + /// Initializes a new instance of the struct. + /// The value to associate with this . [JsonConstructor] - public DebugCollectLogsEntryKind(string value) + public PermissionDecisionOutcome(string value) { ArgumentException.ThrowIfNullOrWhiteSpace(value); _value = value; } - /// Gets the value associated with this . + /// Gets the value associated with this . public string Value => _value ?? string.Empty; - /// Include a single server-local file. - public static DebugCollectLogsEntryKind File { get; } = new("file"); + /// The request was approved automatically without a new human decision. + public static PermissionDecisionOutcome AutoApproved { get; } = new("auto_approved"); - /// Include files from a server-local directory recursively. - public static DebugCollectLogsEntryKind Directory { get; } = new("directory"); + /// The request was denied without an interactive user decision; source records why. + public static PermissionDecisionOutcome AutopilotDenied { get; } = new("autopilot_denied"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(DebugCollectLogsEntryKind left, DebugCollectLogsEntryKind right) => left.Equals(right); + /// The response came from an interactive user prompt. + public static PermissionDecisionOutcome PromptedUser { get; } = new("prompted_user"); - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(DebugCollectLogsEntryKind left, DebugCollectLogsEntryKind right) => !(left == right); + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(PermissionDecisionOutcome left, PermissionDecisionOutcome right) => left.Equals(right); + + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(PermissionDecisionOutcome left, PermissionDecisionOutcome right) => !(left == right); /// - public override bool Equals(object? obj) => obj is DebugCollectLogsEntryKind other && Equals(other); + public override bool Equals(object? obj) => obj is PermissionDecisionOutcome other && Equals(other); /// - public bool Equals(DebugCollectLogsEntryKind other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + public bool Equals(PermissionDecisionOutcome other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); /// public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); @@ -33141,65 +34060,65 @@ public DebugCollectLogsEntryKind(string value) /// public override string ToString() => Value; - /// Provides a for serializing instances. + /// Provides a for serializing instances. [EditorBrowsable(EditorBrowsableState.Never)] - public sealed class Converter : JsonConverter + public sealed class Converter : JsonConverter { /// - public override DebugCollectLogsEntryKind Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override PermissionDecisionOutcome Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); } /// - public override void Write(Utf8JsonWriter writer, DebugCollectLogsEntryKind value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, PermissionDecisionOutcome value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(DebugCollectLogsEntryKind)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(PermissionDecisionOutcome)); } } } -/// How a collected debug entry should be redacted before being staged. +/// Response capability available to the client when it settled a permission request. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct DebugCollectLogsRedaction : IEquatable +public readonly struct PermissionResponseCapability : IEquatable { private readonly string? _value; - /// Initializes a new instance of the struct. - /// The value to associate with this . + /// Initializes a new instance of the struct. + /// The value to associate with this . [JsonConstructor] - public DebugCollectLogsRedaction(string value) + public PermissionResponseCapability(string value) { ArgumentException.ThrowIfNullOrWhiteSpace(value); _value = value; } - /// Gets the value associated with this . + /// Gets the value associated with this . public string Value => _value ?? string.Empty; - /// Redact the file as plain UTF-8 log text. - public static DebugCollectLogsRedaction PlainText { get; } = new("plain-text"); + /// The client could ask a user for this decision. + public static PermissionResponseCapability Interactive { get; } = new("interactive"); - /// Redact each non-empty line as a session event JSON object, falling back to plain-text redaction for malformed lines. - public static DebugCollectLogsRedaction EventsJsonl { get; } = new("events-jsonl"); + /// The client could return an automated response but could not ask a user. + public static PermissionResponseCapability Headless { get; } = new("headless"); - /// No redaction is applied. The caller must ensure any necessary redaction is performed before this call. - public static DebugCollectLogsRedaction None { get; } = new("none"); + /// The client had no response path available. + public static PermissionResponseCapability None { get; } = new("none"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(DebugCollectLogsRedaction left, DebugCollectLogsRedaction right) => left.Equals(right); + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(PermissionResponseCapability left, PermissionResponseCapability right) => left.Equals(right); - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(DebugCollectLogsRedaction left, DebugCollectLogsRedaction right) => !(left == right); + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(PermissionResponseCapability left, PermissionResponseCapability right) => !(left == right); /// - public override bool Equals(object? obj) => obj is DebugCollectLogsRedaction other && Equals(other); + public override bool Equals(object? obj) => obj is PermissionResponseCapability other && Equals(other); /// - public bool Equals(DebugCollectLogsRedaction other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + public bool Equals(PermissionResponseCapability other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); /// public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); @@ -33207,65 +34126,71 @@ public DebugCollectLogsRedaction(string value) /// public override string ToString() => Value; - /// Provides a for serializing instances. + /// Provides a for serializing instances. [EditorBrowsable(EditorBrowsableState.Never)] - public sealed class Converter : JsonConverter + public sealed class Converter : JsonConverter { /// - public override DebugCollectLogsRedaction Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override PermissionResponseCapability Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); } /// - public override void Write(Utf8JsonWriter writer, DebugCollectLogsRedaction value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, PermissionResponseCapability value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(DebugCollectLogsRedaction)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(PermissionResponseCapability)); } } } -/// Cumulative resource ceiling that stopped a workflow run. +/// Client surface that submitted a permission response. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct WorkflowRunFailureKind : IEquatable +public readonly struct PermissionDecisionSurface : IEquatable { private readonly string? _value; - /// Initializes a new instance of the struct. - /// The value to associate with this . + /// Initializes a new instance of the struct. + /// The value to associate with this . [JsonConstructor] - public WorkflowRunFailureKind(string value) + public PermissionDecisionSurface(string value) { ArgumentException.ThrowIfNullOrWhiteSpace(value); _value = value; } - /// Gets the value associated with this . + /// Gets the value associated with this . public string Value => _value ?? string.Empty; - /// The run admitted the approved maximum total number of subagents. - public static WorkflowRunFailureKind MaxTotalSubagents { get; } = new("maxTotalSubagents"); + /// The interactive Copilot CLI terminal UI. + public static PermissionDecisionSurface Tui { get; } = new("tui"); - /// The run reached the approved accumulated active-execution time in seconds. - public static WorkflowRunFailureKind TimeoutSeconds { get; } = new("timeoutSeconds"); + /// The non-interactive Copilot CLI prompt mode. + public static PermissionDecisionSurface PromptMode { get; } = new("prompt_mode"); - /// The run's settled subagent model usage exceeded the approved AI-credit ceiling, or no headroom remained for another subagent. - public static WorkflowRunFailureKind MaxAiCredits { get; } = new("maxAiCredits"); + /// The Copilot App client. + public static PermissionDecisionSurface CopilotApp { get; } = new("copilot_app"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(WorkflowRunFailureKind left, WorkflowRunFailureKind right) => left.Equals(right); + /// An Agent Client Protocol host. + public static PermissionDecisionSurface Acp { get; } = new("acp"); - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(WorkflowRunFailureKind left, WorkflowRunFailureKind right) => !(left == right); + /// A generic Copilot SDK client. + public static PermissionDecisionSurface Sdk { get; } = new("sdk"); + + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(PermissionDecisionSurface left, PermissionDecisionSurface right) => left.Equals(right); + + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(PermissionDecisionSurface left, PermissionDecisionSurface right) => !(left == right); /// - public override bool Equals(object? obj) => obj is WorkflowRunFailureKind other && Equals(other); + public override bool Equals(object? obj) => obj is PermissionDecisionSurface other && Equals(other); /// - public bool Equals(WorkflowRunFailureKind other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + public bool Equals(PermissionDecisionSurface other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); /// public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); @@ -33273,89 +34198,80 @@ public WorkflowRunFailureKind(string value) /// public override string ToString() => Value; - /// Provides a for serializing instances. + /// Provides a for serializing instances. [EditorBrowsable(EditorBrowsableState.Never)] - public sealed class Converter : JsonConverter + public sealed class Converter : JsonConverter { /// - public override WorkflowRunFailureKind Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override PermissionDecisionSurface Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); } /// - public override void Write(Utf8JsonWriter writer, WorkflowRunFailureKind value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, PermissionDecisionSurface value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(WorkflowRunFailureKind)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(PermissionDecisionSurface)); } } } -/// Execution-critical workflow storage operation. +/// Authentication type. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct WorkflowDurableOperation : IEquatable +public readonly struct AuthInfoType : IEquatable { private readonly string? _value; - /// Initializes a new instance of the struct. - /// The value to associate with this . + /// Initializes a new instance of the struct. + /// The value to associate with this . [JsonConstructor] - public WorkflowDurableOperation(string value) + public AuthInfoType(string value) { ArgumentException.ThrowIfNullOrWhiteSpace(value); _value = value; } - /// Gets the value associated with this . + /// Gets the value associated with this . public string Value => _value ?? string.Empty; - /// Creating the durable run and declared phases. - public static WorkflowDurableOperation CreateRun { get; } = new("createRun"); - - /// Persisting the transition to running. - public static WorkflowDurableOperation MarkRunStarted { get; } = new("markRunStarted"); - - /// Persisting the terminal run envelope. - public static WorkflowDurableOperation FinishRun { get; } = new("finishRun"); - - /// Persisting subagent admission accounting. - public static WorkflowDurableOperation ReserveAgent { get; } = new("reserveAgent"); + /// Authentication provided by a GitHub App HMAC credential. + public static AuthInfoType Hmac { get; } = new("hmac"); - /// Rolling back an uncommitted subagent admission. - public static WorkflowDurableOperation ReleaseAgent { get; } = new("releaseAgent"); + /// Authentication resolved from environment-provided credentials. + public static AuthInfoType Env { get; } = new("env"); - /// Persisting an idempotent model-usage charge. - public static WorkflowDurableOperation ChargeCredit { get; } = new("chargeCredit"); + /// Authentication from an interactive user sign-in. + public static AuthInfoType User { get; } = new("user"); - /// Persisting active execution time. - public static WorkflowDurableOperation AddElapsed { get; } = new("addElapsed"); + /// Authentication delegated to the GitHub CLI. + public static AuthInfoType GhCli { get; } = new("gh-cli"); - /// Reading the authoritative AI-credit total. - public static WorkflowDurableOperation ReconcileCreditTotal { get; } = new("reconcileCreditTotal"); + /// Authentication from an API key credential. + public static AuthInfoType ApiKey { get; } = new("api-key"); - /// Reading a journal entry without treating storage failure as a cache miss. - public static WorkflowDurableOperation JournalGet { get; } = new("journalGet"); + /// Authentication from a GitHub token. + public static AuthInfoType Token { get; } = new("token"); - /// Persisting a journal entry before reporting success. - public static WorkflowDurableOperation JournalPut { get; } = new("journalPut"); + /// Authentication from an SDK GitHub token callback. + public static AuthInfoType TokenProvider { get; } = new("token-provider"); - /// Renewing the durable owner lease that proves this process still owns the run. - public static WorkflowDurableOperation RefreshLease { get; } = new("refreshLease"); + /// Authentication from a Copilot API token. + public static AuthInfoType CopilotApiToken { get; } = new("copilot-api-token"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(WorkflowDurableOperation left, WorkflowDurableOperation right) => left.Equals(right); + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(AuthInfoType left, AuthInfoType right) => left.Equals(right); - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(WorkflowDurableOperation left, WorkflowDurableOperation right) => !(left == right); + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(AuthInfoType left, AuthInfoType right) => !(left == right); /// - public override bool Equals(object? obj) => obj is WorkflowDurableOperation other && Equals(other); + public override bool Equals(object? obj) => obj is AuthInfoType other && Equals(other); /// - public bool Equals(WorkflowDurableOperation other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + public bool Equals(AuthInfoType other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); /// public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); @@ -33363,77 +34279,71 @@ public WorkflowDurableOperation(string value) /// public override string ToString() => Value; - /// Provides a for serializing instances. + /// Provides a for serializing instances. [EditorBrowsable(EditorBrowsableState.Never)] - public sealed class Converter : JsonConverter + public sealed class Converter : JsonConverter { /// - public override WorkflowDurableOperation Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override AuthInfoType Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); } /// - public override void Write(Utf8JsonWriter writer, WorkflowDurableOperation value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, AuthInfoType value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(WorkflowDurableOperation)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(AuthInfoType)); } } } -/// Current or terminal state of a workflow run. +/// The provider kind stamped on a signed-in account. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct WorkflowRunStatus : IEquatable +public readonly struct AccountKind : IEquatable { private readonly string? _value; - /// Initializes a new instance of the struct. - /// The value to associate with this . + /// Initializes a new instance of the struct. + /// The value to associate with this . [JsonConstructor] - public WorkflowRunStatus(string value) + public AccountKind(string value) { ArgumentException.ThrowIfNullOrWhiteSpace(value); _value = value; } - /// Gets the value associated with this . + /// Gets the value associated with this . public string Value => _value ?? string.Empty; - /// The run was minted and is awaiting approval. - public static WorkflowRunStatus Pending { get; } = new("pending"); - - /// The run is executing. - public static WorkflowRunStatus Running { get; } = new("running"); - - /// The run completed successfully. - public static WorkflowRunStatus Completed { get; } = new("completed"); + /// An OAuth github.com account. + public static AccountKind GitHubDotCom { get; } = new("githubDotCom"); - /// The run was interrupted while resource budget remained. - public static WorkflowRunStatus Halted { get; } = new("halted"); + /// A GitHub Enterprise Cloud account — a GitHub account on a non-github.com host, e.g. *.ghe.com. + public static AccountKind Proxima { get; } = new("proxima"); - /// The current attempt stopped intentionally and the run may be resumed. - public static WorkflowRunStatus Paused { get; } = new("paused"); + /// A GitHub (EMU) account derived from a base Entra identity. + public static AccountKind EntraEmu { get; } = new("entraEmu"); - /// The run was cancelled before completion. - public static WorkflowRunStatus Cancelled { get; } = new("cancelled"); + /// A base Microsoft Entra identity. + public static AccountKind Entra { get; } = new("entra"); - /// The workflow body failed or reached a cumulative resource ceiling. - public static WorkflowRunStatus Error { get; } = new("error"); + /// A Microsoft 365 Copilot (Loki) inference account derived from the same base Entra identity as an EMU account; its bearer is a Loki-scoped inference token consumed through the model-provider path, not the GitHub switcher. + public static AccountKind Loki { get; } = new("loki"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(WorkflowRunStatus left, WorkflowRunStatus right) => left.Equals(right); + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(AccountKind left, AccountKind right) => left.Equals(right); - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(WorkflowRunStatus left, WorkflowRunStatus right) => !(left == right); + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(AccountKind left, AccountKind right) => !(left == right); /// - public override bool Equals(object? obj) => obj is WorkflowRunStatus other && Equals(other); + public override bool Equals(object? obj) => obj is AccountKind other && Equals(other); /// - public bool Equals(WorkflowRunStatus other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + public bool Equals(AccountKind other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); /// public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); @@ -33441,68 +34351,65 @@ public WorkflowRunStatus(string value) /// public override string ToString() => Value; - /// Provides a for serializing instances. + /// Provides a for serializing instances. [EditorBrowsable(EditorBrowsableState.Never)] - public sealed class Converter : JsonConverter + public sealed class Converter : JsonConverter { /// - public override WorkflowRunStatus Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override AccountKind Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); } /// - public override void Write(Utf8JsonWriter writer, WorkflowRunStatus value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, AccountKind value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(WorkflowRunStatus)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(AccountKind)); } } } -/// Derived lifecycle state of a workflow phase. +/// A provider a consumer may interactively sign in with. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct WorkflowPhaseStatus : IEquatable +public readonly struct LoginProviderKind : IEquatable { private readonly string? _value; - /// Initializes a new instance of the struct. - /// The value to associate with this . + /// Initializes a new instance of the struct. + /// The value to associate with this . [JsonConstructor] - public WorkflowPhaseStatus(string value) + public LoginProviderKind(string value) { ArgumentException.ThrowIfNullOrWhiteSpace(value); _value = value; } - /// Gets the value associated with this . + /// Gets the value associated with this . public string Value => _value ?? string.Empty; - /// The phase has not been entered yet. - public static WorkflowPhaseStatus Pending { get; } = new("pending"); - - /// The phase is currently entered and accumulating active time. - public static WorkflowPhaseStatus Active { get; } = new("active"); + /// OAuth github.com sign-in via the browser (web loopback + PKCE). + public static LoginProviderKind GitHubDotCom { get; } = new("githubDotCom"); - /// The phase was entered and has since been closed. - public static WorkflowPhaseStatus Completed { get; } = new("completed"); + /// A GitHub Enterprise Cloud account — a GitHub account on a non-github.com host, e.g. *.ghe.com; the host is supplied interactively through the neutral input-required step. + public static LoginProviderKind Proxima { get; } = new("proxima"); - /// The phase was never entered because a later phase was entered or the run reached a terminal state. - public static WorkflowPhaseStatus Skipped { get; } = new("skipped"); + /// Microsoft Entra sign-in that derives a GitHub (EMU) credential. + public static LoginProviderKind Entra { get; } = new("entra"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(WorkflowPhaseStatus left, WorkflowPhaseStatus right) => left.Equals(right); + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(LoginProviderKind left, LoginProviderKind right) => left.Equals(right); - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(WorkflowPhaseStatus left, WorkflowPhaseStatus right) => !(left == right); + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(LoginProviderKind left, LoginProviderKind right) => !(left == right); /// - public override bool Equals(object? obj) => obj is WorkflowPhaseStatus other && Equals(other); + public override bool Equals(object? obj) => obj is LoginProviderKind other && Equals(other); /// - public bool Equals(WorkflowPhaseStatus other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + public bool Equals(LoginProviderKind other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); /// public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); @@ -33510,62 +34417,65 @@ public WorkflowPhaseStatus(string value) /// public override string ToString() => Value; - /// Provides a for serializing instances. + /// Provides a for serializing instances. [EditorBrowsable(EditorBrowsableState.Never)] - public sealed class Converter : JsonConverter + public sealed class Converter : JsonConverter { /// - public override WorkflowPhaseStatus Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override LoginProviderKind Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); } /// - public override void Write(Utf8JsonWriter writer, WorkflowPhaseStatus value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, LoginProviderKind value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(WorkflowPhaseStatus)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(LoginProviderKind)); } } } -/// Kind of workflow progress line. +/// Terminal disposition of a login persistence attempt. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct WorkflowLogLineKind : IEquatable +public readonly struct AuthLoginResultStatus : IEquatable { private readonly string? _value; - /// Initializes a new instance of the struct. - /// The value to associate with this . + /// Initializes a new instance of the struct. + /// The value to associate with this . [JsonConstructor] - public WorkflowLogLineKind(string value) + public AuthLoginResultStatus(string value) { ArgumentException.ThrowIfNullOrWhiteSpace(value); _value = value; } - /// Gets the value associated with this . + /// Gets the value associated with this . public string Value => _value ?? string.Empty; - /// A narrator log line. - public static WorkflowLogLineKind Log { get; } = new("log"); + /// The credential was persisted and the account is signed in. + public static AuthLoginResultStatus Completed { get; } = new("completed"); - /// A named workflow phase marker. - public static WorkflowLogLineKind Phase { get; } = new("phase"); + /// Persistence needs explicit consent to store the token in plaintext. + public static AuthLoginResultStatus NeedsPlaintextConsent { get; } = new("needs-plaintext-consent"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(WorkflowLogLineKind left, WorkflowLogLineKind right) => left.Equals(right); + /// The user declined plaintext persistence. + public static AuthLoginResultStatus Declined { get; } = new("declined"); - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(WorkflowLogLineKind left, WorkflowLogLineKind right) => !(left == right); + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(AuthLoginResultStatus left, AuthLoginResultStatus right) => left.Equals(right); + + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(AuthLoginResultStatus left, AuthLoginResultStatus right) => !(left == right); /// - public override bool Equals(object? obj) => obj is WorkflowLogLineKind other && Equals(other); + public override bool Equals(object? obj) => obj is AuthLoginResultStatus other && Equals(other); /// - public bool Equals(WorkflowLogLineKind other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + public bool Equals(AuthLoginResultStatus other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); /// public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); @@ -33573,62 +34483,68 @@ public WorkflowLogLineKind(string value) /// public override string ToString() => Value; - /// Provides a for serializing instances. + /// Provides a for serializing instances. [EditorBrowsable(EditorBrowsableState.Never)] - public sealed class Converter : JsonConverter + public sealed class Converter : JsonConverter { /// - public override WorkflowLogLineKind Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override AuthLoginResultStatus Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); } /// - public override void Write(Utf8JsonWriter writer, WorkflowLogLineKind value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, AuthLoginResultStatus value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(WorkflowLogLineKind)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(AuthLoginResultStatus)); } } } -/// Action the runtime selected for a durable workflow pause checkpoint. +/// Source category for a collected debug bundle entry. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct WorkflowPauseCheckpointAction : IEquatable +public readonly struct DebugCollectLogsSource : IEquatable { private readonly string? _value; - /// Initializes a new instance of the struct. - /// The value to associate with this . + /// Initializes a new instance of the struct. + /// The value to associate with this . [JsonConstructor] - public WorkflowPauseCheckpointAction(string value) + public DebugCollectLogsSource(string value) { ArgumentException.ThrowIfNullOrWhiteSpace(value); _value = value; } - /// Gets the value associated with this . + /// Gets the value associated with this . public string Value => _value ?? string.Empty; - /// The checkpoint was committed by a prior paused attempt, so execution may continue. - public static WorkflowPauseCheckpointAction Continue { get; } = new("continue"); + /// Session event log. + public static DebugCollectLogsSource Events { get; } = new("events"); + + /// Process log for the session. + public static DebugCollectLogsSource ProcessLog { get; } = new("process-log"); + + /// Interactive shell log for the session. + public static DebugCollectLogsSource ShellLog { get; } = new("shell-log"); - /// This attempt claimed the checkpoint and must cooperatively stop. - public static WorkflowPauseCheckpointAction Pause { get; } = new("pause"); + /// Caller-provided diagnostic entry. + public static DebugCollectLogsSource Additional { get; } = new("additional"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(WorkflowPauseCheckpointAction left, WorkflowPauseCheckpointAction right) => left.Equals(right); + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(DebugCollectLogsSource left, DebugCollectLogsSource right) => left.Equals(right); - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(WorkflowPauseCheckpointAction left, WorkflowPauseCheckpointAction right) => !(left == right); + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(DebugCollectLogsSource left, DebugCollectLogsSource right) => !(left == right); /// - public override bool Equals(object? obj) => obj is WorkflowPauseCheckpointAction other && Equals(other); + public override bool Equals(object? obj) => obj is DebugCollectLogsSource other && Equals(other); /// - public bool Equals(WorkflowPauseCheckpointAction other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + public bool Equals(DebugCollectLogsSource other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); /// public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); @@ -33636,62 +34552,62 @@ public WorkflowPauseCheckpointAction(string value) /// public override string ToString() => Value; - /// Provides a for serializing instances. + /// Provides a for serializing instances. [EditorBrowsable(EditorBrowsableState.Never)] - public sealed class Converter : JsonConverter + public sealed class Converter : JsonConverter { /// - public override WorkflowPauseCheckpointAction Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override DebugCollectLogsSource Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); } /// - public override void Write(Utf8JsonWriter writer, WorkflowPauseCheckpointAction value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, DebugCollectLogsSource value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(WorkflowPauseCheckpointAction)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(DebugCollectLogsSource)); } } } -/// Whether the requested preference was already effective or was accepted for later transactional activation. +/// Destination kind that was written. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct ModelSwitchAutoTierStatus : IEquatable +public readonly struct DebugCollectLogsResultKind : IEquatable { private readonly string? _value; - /// Initializes a new instance of the struct. - /// The value to associate with this . + /// Initializes a new instance of the struct. + /// The value to associate with this . [JsonConstructor] - public ModelSwitchAutoTierStatus(string value) + public DebugCollectLogsResultKind(string value) { ArgumentException.ThrowIfNullOrWhiteSpace(value); _value = value; } - /// Gets the value associated with this . + /// Gets the value associated with this . public string Value => _value ?? string.Empty; - /// The requested preference is already effective. No activation is pending for it, although this request may have cancelled an earlier unclaimed preference reported in `supersededAutoTier`. - public static ModelSwitchAutoTierStatus Unchanged { get; } = new("unchanged"); + /// A .tgz archive was written. + public static DebugCollectLogsResultKind Archive { get; } = new("archive"); - /// The request was accepted but has not committed. A later user turn using the `auto` model must mint and validate the replacement before it becomes effective. - public static ModelSwitchAutoTierStatus Pending { get; } = new("pending"); + /// A directory containing the collected files was written. + public static DebugCollectLogsResultKind Directory { get; } = new("directory"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(ModelSwitchAutoTierStatus left, ModelSwitchAutoTierStatus right) => left.Equals(right); + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(DebugCollectLogsResultKind left, DebugCollectLogsResultKind right) => left.Equals(right); - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(ModelSwitchAutoTierStatus left, ModelSwitchAutoTierStatus right) => !(left == right); + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(DebugCollectLogsResultKind left, DebugCollectLogsResultKind right) => !(left == right); /// - public override bool Equals(object? obj) => obj is ModelSwitchAutoTierStatus other && Equals(other); + public override bool Equals(object? obj) => obj is DebugCollectLogsResultKind other && Equals(other); /// - public bool Equals(ModelSwitchAutoTierStatus other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + public bool Equals(DebugCollectLogsResultKind other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); /// public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); @@ -33699,62 +34615,62 @@ public ModelSwitchAutoTierStatus(string value) /// public override string ToString() => Value; - /// Provides a for serializing instances. + /// Provides a for serializing instances. [EditorBrowsable(EditorBrowsableState.Never)] - public sealed class Converter : JsonConverter + public sealed class Converter : JsonConverter { /// - public override ModelSwitchAutoTierStatus Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override DebugCollectLogsResultKind Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); } /// - public override void Write(Utf8JsonWriter writer, ModelSwitchAutoTierStatus value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, DebugCollectLogsResultKind value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(ModelSwitchAutoTierStatus)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(DebugCollectLogsResultKind)); } } } -/// Allowed values for the `WorkspacesWorkspaceDetailsHostType` enumeration. +/// Kind of caller-provided debug log entry. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct WorkspacesWorkspaceDetailsHostType : IEquatable +public readonly struct DebugCollectLogsEntryKind : IEquatable { private readonly string? _value; - /// Initializes a new instance of the struct. - /// The value to associate with this . + /// Initializes a new instance of the struct. + /// The value to associate with this . [JsonConstructor] - public WorkspacesWorkspaceDetailsHostType(string value) + public DebugCollectLogsEntryKind(string value) { ArgumentException.ThrowIfNullOrWhiteSpace(value); _value = value; } - /// Gets the value associated with this . + /// Gets the value associated with this . public string Value => _value ?? string.Empty; - /// Workspace repository is hosted on GitHub. - public static WorkspacesWorkspaceDetailsHostType GitHub { get; } = new("github"); + /// Include a single server-local file. + public static DebugCollectLogsEntryKind File { get; } = new("file"); - /// Workspace repository is hosted on Azure DevOps. - public static WorkspacesWorkspaceDetailsHostType Ado { get; } = new("ado"); + /// Include files from a server-local directory recursively. + public static DebugCollectLogsEntryKind Directory { get; } = new("directory"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(WorkspacesWorkspaceDetailsHostType left, WorkspacesWorkspaceDetailsHostType right) => left.Equals(right); + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(DebugCollectLogsEntryKind left, DebugCollectLogsEntryKind right) => left.Equals(right); - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(WorkspacesWorkspaceDetailsHostType left, WorkspacesWorkspaceDetailsHostType right) => !(left == right); + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(DebugCollectLogsEntryKind left, DebugCollectLogsEntryKind right) => !(left == right); /// - public override bool Equals(object? obj) => obj is WorkspacesWorkspaceDetailsHostType other && Equals(other); + public override bool Equals(object? obj) => obj is DebugCollectLogsEntryKind other && Equals(other); /// - public bool Equals(WorkspacesWorkspaceDetailsHostType other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + public bool Equals(DebugCollectLogsEntryKind other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); /// public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); @@ -33762,68 +34678,65 @@ public WorkspacesWorkspaceDetailsHostType(string value) /// public override string ToString() => Value; - /// Provides a for serializing instances. + /// Provides a for serializing instances. [EditorBrowsable(EditorBrowsableState.Never)] - public sealed class Converter : JsonConverter + public sealed class Converter : JsonConverter { /// - public override WorkspacesWorkspaceDetailsHostType Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override DebugCollectLogsEntryKind Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); } /// - public override void Write(Utf8JsonWriter writer, WorkspacesWorkspaceDetailsHostType value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, DebugCollectLogsEntryKind value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(WorkspacesWorkspaceDetailsHostType)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(DebugCollectLogsEntryKind)); } } } -/// Type of change represented by this file diff. +/// How a collected debug entry should be redacted before being staged. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct WorkspaceDiffFileChangeType : IEquatable +public readonly struct DebugCollectLogsRedaction : IEquatable { private readonly string? _value; - /// Initializes a new instance of the struct. - /// The value to associate with this . + /// Initializes a new instance of the struct. + /// The value to associate with this . [JsonConstructor] - public WorkspaceDiffFileChangeType(string value) + public DebugCollectLogsRedaction(string value) { ArgumentException.ThrowIfNullOrWhiteSpace(value); _value = value; } - /// Gets the value associated with this . + /// Gets the value associated with this . public string Value => _value ?? string.Empty; - /// The file was added. - public static WorkspaceDiffFileChangeType Added { get; } = new("added"); - - /// The file was modified. - public static WorkspaceDiffFileChangeType Modified { get; } = new("modified"); + /// Redact the file as plain UTF-8 log text. + public static DebugCollectLogsRedaction PlainText { get; } = new("plain-text"); - /// The file was deleted. - public static WorkspaceDiffFileChangeType Deleted { get; } = new("deleted"); + /// Redact each non-empty line as a session event JSON object, falling back to plain-text redaction for malformed lines. + public static DebugCollectLogsRedaction EventsJsonl { get; } = new("events-jsonl"); - /// The file was renamed. - public static WorkspaceDiffFileChangeType Renamed { get; } = new("renamed"); + /// No redaction is applied. The caller must ensure any necessary redaction is performed before this call. + public static DebugCollectLogsRedaction None { get; } = new("none"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(WorkspaceDiffFileChangeType left, WorkspaceDiffFileChangeType right) => left.Equals(right); + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(DebugCollectLogsRedaction left, DebugCollectLogsRedaction right) => left.Equals(right); - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(WorkspaceDiffFileChangeType left, WorkspaceDiffFileChangeType right) => !(left == right); + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(DebugCollectLogsRedaction left, DebugCollectLogsRedaction right) => !(left == right); /// - public override bool Equals(object? obj) => obj is WorkspaceDiffFileChangeType other && Equals(other); + public override bool Equals(object? obj) => obj is DebugCollectLogsRedaction other && Equals(other); /// - public bool Equals(WorkspaceDiffFileChangeType other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + public bool Equals(DebugCollectLogsRedaction other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); /// public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); @@ -33831,65 +34744,65 @@ public WorkspaceDiffFileChangeType(string value) /// public override string ToString() => Value; - /// Provides a for serializing instances. + /// Provides a for serializing instances. [EditorBrowsable(EditorBrowsableState.Never)] - public sealed class Converter : JsonConverter + public sealed class Converter : JsonConverter { /// - public override WorkspaceDiffFileChangeType Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override DebugCollectLogsRedaction Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); } /// - public override void Write(Utf8JsonWriter writer, WorkspaceDiffFileChangeType value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, DebugCollectLogsRedaction value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(WorkspaceDiffFileChangeType)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(DebugCollectLogsRedaction)); } } } -/// Diff mode requested by the client. +/// Cumulative resource ceiling that stopped a workflow run. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct WorkspaceDiffMode : IEquatable +public readonly struct WorkflowRunFailureKind : IEquatable { private readonly string? _value; - /// Initializes a new instance of the struct. - /// The value to associate with this . + /// Initializes a new instance of the struct. + /// The value to associate with this . [JsonConstructor] - public WorkspaceDiffMode(string value) + public WorkflowRunFailureKind(string value) { ArgumentException.ThrowIfNullOrWhiteSpace(value); _value = value; } - /// Gets the value associated with this . + /// Gets the value associated with this . public string Value => _value ?? string.Empty; - /// Return staged, unstaged, and untracked working tree changes. - public static WorkspaceDiffMode Unstaged { get; } = new("unstaged"); + /// The run admitted the approved maximum total number of subagents. + public static WorkflowRunFailureKind MaxTotalSubagents { get; } = new("maxTotalSubagents"); - /// Return changes compared with the default branch. - public static WorkspaceDiffMode Branch { get; } = new("branch"); + /// The run reached the approved accumulated active-execution time in seconds. + public static WorkflowRunFailureKind TimeoutSeconds { get; } = new("timeoutSeconds"); - /// Return the cumulative diff of files Copilot changed this session (used in non-git workspaces). - public static WorkspaceDiffMode Session { get; } = new("session"); + /// The run's settled subagent model usage exceeded the approved AI-credit ceiling, or no headroom remained for another subagent. + public static WorkflowRunFailureKind MaxAiCredits { get; } = new("maxAiCredits"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(WorkspaceDiffMode left, WorkspaceDiffMode right) => left.Equals(right); + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(WorkflowRunFailureKind left, WorkflowRunFailureKind right) => left.Equals(right); - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(WorkspaceDiffMode left, WorkspaceDiffMode right) => !(left == right); + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(WorkflowRunFailureKind left, WorkflowRunFailureKind right) => !(left == right); /// - public override bool Equals(object? obj) => obj is WorkspaceDiffMode other && Equals(other); + public override bool Equals(object? obj) => obj is WorkflowRunFailureKind other && Equals(other); /// - public bool Equals(WorkspaceDiffMode other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + public bool Equals(WorkflowRunFailureKind other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); /// public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); @@ -33897,65 +34810,89 @@ public WorkspaceDiffMode(string value) /// public override string ToString() => Value; - /// Provides a for serializing instances. + /// Provides a for serializing instances. [EditorBrowsable(EditorBrowsableState.Never)] - public sealed class Converter : JsonConverter + public sealed class Converter : JsonConverter { /// - public override WorkspaceDiffMode Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override WorkflowRunFailureKind Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); } /// - public override void Write(Utf8JsonWriter writer, WorkspaceDiffMode value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, WorkflowRunFailureKind value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(WorkspaceDiffMode)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(WorkflowRunFailureKind)); } } } -/// Reason a rewind read (rewind points, file-restore preview, or session diff) could not be answered from the session's file-change captures. +/// Execution-critical workflow storage operation. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct HistoryRewindUnavailableReason : IEquatable +public readonly struct WorkflowDurableOperation : IEquatable { private readonly string? _value; - /// Initializes a new instance of the struct. - /// The value to associate with this . + /// Initializes a new instance of the struct. + /// The value to associate with this . [JsonConstructor] - public HistoryRewindUnavailableReason(string value) + public WorkflowDurableOperation(string value) { ArgumentException.ThrowIfNullOrWhiteSpace(value); _value = value; } - /// Gets the value associated with this . + /// Gets the value associated with this . public string Value => _value ?? string.Empty; - /// The session did not opt into file-change tracking before its first turn. - public static HistoryRewindUnavailableReason FileChangeTrackingDisabled { get; } = new("file-change-tracking-disabled"); + /// Creating the durable run and declared phases. + public static WorkflowDurableOperation CreateRun { get; } = new("createRun"); - /// The session still has work that may mutate files or history. Transient: the same request succeeds once the session settles, so callers should retry rather than treat it as a failure. - public static HistoryRewindUnavailableReason SessionBusy { get; } = new("session-busy"); + /// Persisting the transition to running. + public static WorkflowDurableOperation MarkRunStarted { get; } = new("markRunStarted"); - /// Remote-backed rewind routing is not supported. - public static HistoryRewindUnavailableReason UnsupportedRemoteSession { get; } = new("unsupported-remote-session"); + /// Persisting the terminal run envelope. + public static WorkflowDurableOperation FinishRun { get; } = new("finishRun"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(HistoryRewindUnavailableReason left, HistoryRewindUnavailableReason right) => left.Equals(right); + /// Persisting subagent admission accounting. + public static WorkflowDurableOperation ReserveAgent { get; } = new("reserveAgent"); - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(HistoryRewindUnavailableReason left, HistoryRewindUnavailableReason right) => !(left == right); + /// Rolling back an uncommitted subagent admission. + public static WorkflowDurableOperation ReleaseAgent { get; } = new("releaseAgent"); + + /// Persisting an idempotent model-usage charge. + public static WorkflowDurableOperation ChargeCredit { get; } = new("chargeCredit"); + + /// Persisting active execution time. + public static WorkflowDurableOperation AddElapsed { get; } = new("addElapsed"); + + /// Reading the authoritative AI-credit total. + public static WorkflowDurableOperation ReconcileCreditTotal { get; } = new("reconcileCreditTotal"); + + /// Reading a journal entry without treating storage failure as a cache miss. + public static WorkflowDurableOperation JournalGet { get; } = new("journalGet"); + + /// Persisting a journal entry before reporting success. + public static WorkflowDurableOperation JournalPut { get; } = new("journalPut"); + + /// Renewing the durable owner lease that proves this process still owns the run. + public static WorkflowDurableOperation RefreshLease { get; } = new("refreshLease"); + + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(WorkflowDurableOperation left, WorkflowDurableOperation right) => left.Equals(right); + + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(WorkflowDurableOperation left, WorkflowDurableOperation right) => !(left == right); /// - public override bool Equals(object? obj) => obj is HistoryRewindUnavailableReason other && Equals(other); + public override bool Equals(object? obj) => obj is WorkflowDurableOperation other && Equals(other); /// - public bool Equals(HistoryRewindUnavailableReason other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + public bool Equals(WorkflowDurableOperation other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); /// public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); @@ -33963,65 +34900,77 @@ public HistoryRewindUnavailableReason(string value) /// public override string ToString() => Value; - /// Provides a for serializing instances. + /// Provides a for serializing instances. [EditorBrowsable(EditorBrowsableState.Never)] - public sealed class Converter : JsonConverter + public sealed class Converter : JsonConverter { /// - public override HistoryRewindUnavailableReason Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override WorkflowDurableOperation Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); } /// - public override void Write(Utf8JsonWriter writer, HistoryRewindUnavailableReason value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, WorkflowDurableOperation value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(HistoryRewindUnavailableReason)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(WorkflowDurableOperation)); } } } -/// Current normalized autopilot objective lifecycle status. +/// Current or terminal state of a workflow run. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct AutopilotObjectiveStatus : IEquatable +public readonly struct WorkflowRunStatus : IEquatable { private readonly string? _value; - /// Initializes a new instance of the struct. - /// The value to associate with this . + /// Initializes a new instance of the struct. + /// The value to associate with this . [JsonConstructor] - public AutopilotObjectiveStatus(string value) + public WorkflowRunStatus(string value) { ArgumentException.ThrowIfNullOrWhiteSpace(value); _value = value; } - /// Gets the value associated with this . + /// Gets the value associated with this . public string Value => _value ?? string.Empty; - /// The objective is actively running. - public static AutopilotObjectiveStatus Active { get; } = new("active"); + /// The run was minted and is awaiting approval. + public static WorkflowRunStatus Pending { get; } = new("pending"); - /// The objective is paused and may be resumed. - public static AutopilotObjectiveStatus Paused { get; } = new("paused"); + /// The run is executing. + public static WorkflowRunStatus Running { get; } = new("running"); - /// The objective completed. - public static AutopilotObjectiveStatus Completed { get; } = new("completed"); + /// The run completed successfully. + public static WorkflowRunStatus Completed { get; } = new("completed"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(AutopilotObjectiveStatus left, AutopilotObjectiveStatus right) => left.Equals(right); + /// The run was interrupted while resource budget remained. + public static WorkflowRunStatus Halted { get; } = new("halted"); - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(AutopilotObjectiveStatus left, AutopilotObjectiveStatus right) => !(left == right); + /// The current attempt stopped intentionally and the run may be resumed. + public static WorkflowRunStatus Paused { get; } = new("paused"); + + /// The run was cancelled before completion. + public static WorkflowRunStatus Cancelled { get; } = new("cancelled"); + + /// The workflow body failed or reached a cumulative resource ceiling. + public static WorkflowRunStatus Error { get; } = new("error"); + + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(WorkflowRunStatus left, WorkflowRunStatus right) => left.Equals(right); + + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(WorkflowRunStatus left, WorkflowRunStatus right) => !(left == right); /// - public override bool Equals(object? obj) => obj is AutopilotObjectiveStatus other && Equals(other); + public override bool Equals(object? obj) => obj is WorkflowRunStatus other && Equals(other); /// - public bool Equals(AutopilotObjectiveStatus other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + public bool Equals(WorkflowRunStatus other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); /// public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); @@ -34029,62 +34978,68 @@ public AutopilotObjectiveStatus(string value) /// public override string ToString() => Value; - /// Provides a for serializing instances. + /// Provides a for serializing instances. [EditorBrowsable(EditorBrowsableState.Never)] - public sealed class Converter : JsonConverter + public sealed class Converter : JsonConverter { /// - public override AutopilotObjectiveStatus Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override WorkflowRunStatus Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); } /// - public override void Write(Utf8JsonWriter writer, AutopilotObjectiveStatus value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, WorkflowRunStatus value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(AutopilotObjectiveStatus)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(WorkflowRunStatus)); } } } -/// Whether task execution is synchronously awaited or managed in the background. +/// Derived lifecycle state of a workflow phase. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct TaskExecutionMode : IEquatable +public readonly struct WorkflowPhaseStatus : IEquatable { private readonly string? _value; - /// Initializes a new instance of the struct. - /// The value to associate with this . + /// Initializes a new instance of the struct. + /// The value to associate with this . [JsonConstructor] - public TaskExecutionMode(string value) + public WorkflowPhaseStatus(string value) { ArgumentException.ThrowIfNullOrWhiteSpace(value); _value = value; } - /// Gets the value associated with this . + /// Gets the value associated with this . public string Value => _value ?? string.Empty; - /// The task was started with synchronous waiting. - public static TaskExecutionMode Sync { get; } = new("sync"); + /// The phase has not been entered yet. + public static WorkflowPhaseStatus Pending { get; } = new("pending"); - /// The task is managed in the background. - public static TaskExecutionMode Background { get; } = new("background"); + /// The phase is currently entered and accumulating active time. + public static WorkflowPhaseStatus Active { get; } = new("active"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(TaskExecutionMode left, TaskExecutionMode right) => left.Equals(right); + /// The phase was entered and has since been closed. + public static WorkflowPhaseStatus Completed { get; } = new("completed"); - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(TaskExecutionMode left, TaskExecutionMode right) => !(left == right); + /// The phase was never entered because a later phase was entered or the run reached a terminal state. + public static WorkflowPhaseStatus Skipped { get; } = new("skipped"); + + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(WorkflowPhaseStatus left, WorkflowPhaseStatus right) => left.Equals(right); + + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(WorkflowPhaseStatus left, WorkflowPhaseStatus right) => !(left == right); /// - public override bool Equals(object? obj) => obj is TaskExecutionMode other && Equals(other); + public override bool Equals(object? obj) => obj is WorkflowPhaseStatus other && Equals(other); /// - public bool Equals(TaskExecutionMode other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + public bool Equals(WorkflowPhaseStatus other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); /// public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); @@ -34092,71 +35047,62 @@ public TaskExecutionMode(string value) /// public override string ToString() => Value; - /// Provides a for serializing instances. + /// Provides a for serializing instances. [EditorBrowsable(EditorBrowsableState.Never)] - public sealed class Converter : JsonConverter + public sealed class Converter : JsonConverter { /// - public override TaskExecutionMode Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override WorkflowPhaseStatus Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); } /// - public override void Write(Utf8JsonWriter writer, TaskExecutionMode value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, WorkflowPhaseStatus value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(TaskExecutionMode)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(WorkflowPhaseStatus)); } } } -/// Current lifecycle status of the task. +/// Kind of workflow progress line. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct TaskStatus : IEquatable +public readonly struct WorkflowLogLineKind : IEquatable { private readonly string? _value; - /// Initializes a new instance of the struct. - /// The value to associate with this . + /// Initializes a new instance of the struct. + /// The value to associate with this . [JsonConstructor] - public TaskStatus(string value) + public WorkflowLogLineKind(string value) { ArgumentException.ThrowIfNullOrWhiteSpace(value); _value = value; } - /// Gets the value associated with this . + /// Gets the value associated with this . public string Value => _value ?? string.Empty; - /// The task is actively executing. - public static TaskStatus Running { get; } = new("running"); - - /// The task is waiting for additional input. - public static TaskStatus Idle { get; } = new("idle"); - - /// The task finished successfully. - public static TaskStatus Completed { get; } = new("completed"); - - /// The task finished with an error. - public static TaskStatus Failed { get; } = new("failed"); + /// A narrator log line. + public static WorkflowLogLineKind Log { get; } = new("log"); - /// The task was cancelled before completion. - public static TaskStatus Cancelled { get; } = new("cancelled"); + /// A named workflow phase marker. + public static WorkflowLogLineKind Phase { get; } = new("phase"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(TaskStatus left, TaskStatus right) => left.Equals(right); + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(WorkflowLogLineKind left, WorkflowLogLineKind right) => left.Equals(right); - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(TaskStatus left, TaskStatus right) => !(left == right); + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(WorkflowLogLineKind left, WorkflowLogLineKind right) => !(left == right); /// - public override bool Equals(object? obj) => obj is TaskStatus other && Equals(other); + public override bool Equals(object? obj) => obj is WorkflowLogLineKind other && Equals(other); /// - public bool Equals(TaskStatus other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + public bool Equals(WorkflowLogLineKind other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); /// public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); @@ -34164,59 +35110,62 @@ public TaskStatus(string value) /// public override string ToString() => Value; - /// Provides a for serializing instances. + /// Provides a for serializing instances. [EditorBrowsable(EditorBrowsableState.Never)] - public sealed class Converter : JsonConverter + public sealed class Converter : JsonConverter { /// - public override TaskStatus Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override WorkflowLogLineKind Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); } /// - public override void Write(Utf8JsonWriter writer, TaskStatus value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, WorkflowLogLineKind value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(TaskStatus)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(WorkflowLogLineKind)); } } } -/// Client-owned tasks always execute outside the runtime in background mode. +/// Action the runtime selected for a durable workflow pause checkpoint. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct TaskClientExecutionMode : IEquatable +public readonly struct WorkflowPauseCheckpointAction : IEquatable { private readonly string? _value; - /// Initializes a new instance of the struct. - /// The value to associate with this . + /// Initializes a new instance of the struct. + /// The value to associate with this . [JsonConstructor] - public TaskClientExecutionMode(string value) + public WorkflowPauseCheckpointAction(string value) { ArgumentException.ThrowIfNullOrWhiteSpace(value); _value = value; } - /// Gets the value associated with this . + /// Gets the value associated with this . public string Value => _value ?? string.Empty; - /// Gets the background value. - public static TaskClientExecutionMode Background { get; } = new("background"); + /// The checkpoint was committed by a prior paused attempt, so execution may continue. + public static WorkflowPauseCheckpointAction Continue { get; } = new("continue"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(TaskClientExecutionMode left, TaskClientExecutionMode right) => left.Equals(right); + /// This attempt claimed the checkpoint and must cooperatively stop. + public static WorkflowPauseCheckpointAction Pause { get; } = new("pause"); - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(TaskClientExecutionMode left, TaskClientExecutionMode right) => !(left == right); + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(WorkflowPauseCheckpointAction left, WorkflowPauseCheckpointAction right) => left.Equals(right); + + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(WorkflowPauseCheckpointAction left, WorkflowPauseCheckpointAction right) => !(left == right); /// - public override bool Equals(object? obj) => obj is TaskClientExecutionMode other && Equals(other); + public override bool Equals(object? obj) => obj is WorkflowPauseCheckpointAction other && Equals(other); /// - public bool Equals(TaskClientExecutionMode other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + public bool Equals(WorkflowPauseCheckpointAction other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); /// public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); @@ -34224,62 +35173,62 @@ public TaskClientExecutionMode(string value) /// public override string ToString() => Value; - /// Provides a for serializing instances. + /// Provides a for serializing instances. [EditorBrowsable(EditorBrowsableState.Never)] - public sealed class Converter : JsonConverter + public sealed class Converter : JsonConverter { /// - public override TaskClientExecutionMode Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override WorkflowPauseCheckpointAction Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); } /// - public override void Write(Utf8JsonWriter writer, TaskClientExecutionMode value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, WorkflowPauseCheckpointAction value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(TaskClientExecutionMode)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(WorkflowPauseCheckpointAction)); } } } -/// Connection class owning a client task. +/// Whether the requested preference was already effective or was accepted for later transactional activation. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct TaskClientOwnerKind : IEquatable +public readonly struct ModelSwitchAutoTierStatus : IEquatable { private readonly string? _value; - /// Initializes a new instance of the struct. - /// The value to associate with this . + /// Initializes a new instance of the struct. + /// The value to associate with this . [JsonConstructor] - public TaskClientOwnerKind(string value) + public ModelSwitchAutoTierStatus(string value) { ArgumentException.ThrowIfNullOrWhiteSpace(value); _value = value; } - /// Gets the value associated with this . + /// Gets the value associated with this . public string Value => _value ?? string.Empty; - /// A discovered extension connection owns the task. - public static TaskClientOwnerKind Extension { get; } = new("extension"); + /// The requested preference is already effective. No activation is pending for it, although this request may have cancelled an earlier unclaimed preference reported in `supersededAutoTier`. + public static ModelSwitchAutoTierStatus Unchanged { get; } = new("unchanged"); - /// A generic SDK connection owns the task. - public static TaskClientOwnerKind Sdk { get; } = new("sdk"); + /// The request was accepted but has not committed. A later user turn using the `auto` model must mint and validate the replacement before it becomes effective. + public static ModelSwitchAutoTierStatus Pending { get; } = new("pending"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(TaskClientOwnerKind left, TaskClientOwnerKind right) => left.Equals(right); + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(ModelSwitchAutoTierStatus left, ModelSwitchAutoTierStatus right) => left.Equals(right); - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(TaskClientOwnerKind left, TaskClientOwnerKind right) => !(left == right); + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(ModelSwitchAutoTierStatus left, ModelSwitchAutoTierStatus right) => !(left == right); /// - public override bool Equals(object? obj) => obj is TaskClientOwnerKind other && Equals(other); + public override bool Equals(object? obj) => obj is ModelSwitchAutoTierStatus other && Equals(other); /// - public bool Equals(TaskClientOwnerKind other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + public bool Equals(ModelSwitchAutoTierStatus other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); /// public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); @@ -34287,62 +35236,62 @@ public TaskClientOwnerKind(string value) /// public override string ToString() => Value; - /// Provides a for serializing instances. + /// Provides a for serializing instances. [EditorBrowsable(EditorBrowsableState.Never)] - public sealed class Converter : JsonConverter + public sealed class Converter : JsonConverter { /// - public override TaskClientOwnerKind Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override ModelSwitchAutoTierStatus Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); } /// - public override void Write(Utf8JsonWriter writer, TaskClientOwnerKind value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, ModelSwitchAutoTierStatus value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(TaskClientOwnerKind)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(ModelSwitchAutoTierStatus)); } } } -/// Presence of the task's bound join. +/// Allowed values for the `WorkspacesWorkspaceDetailsHostType` enumeration. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct TaskClientOwnerPresence : IEquatable +public readonly struct WorkspacesWorkspaceDetailsHostType : IEquatable { private readonly string? _value; - /// Initializes a new instance of the struct. - /// The value to associate with this . + /// Initializes a new instance of the struct. + /// The value to associate with this . [JsonConstructor] - public TaskClientOwnerPresence(string value) + public WorkspacesWorkspaceDetailsHostType(string value) { ArgumentException.ThrowIfNullOrWhiteSpace(value); _value = value; } - /// Gets the value associated with this . + /// Gets the value associated with this . public string Value => _value ?? string.Empty; - /// The bound session join is connected. - public static TaskClientOwnerPresence Connected { get; } = new("connected"); + /// Workspace repository is hosted on GitHub. + public static WorkspacesWorkspaceDetailsHostType GitHub { get; } = new("github"); - /// The bound session join is disconnected. - public static TaskClientOwnerPresence Disconnected { get; } = new("disconnected"); + /// Workspace repository is hosted on Azure DevOps. + public static WorkspacesWorkspaceDetailsHostType Ado { get; } = new("ado"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(TaskClientOwnerPresence left, TaskClientOwnerPresence right) => left.Equals(right); + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(WorkspacesWorkspaceDetailsHostType left, WorkspacesWorkspaceDetailsHostType right) => left.Equals(right); - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(TaskClientOwnerPresence left, TaskClientOwnerPresence right) => !(left == right); + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(WorkspacesWorkspaceDetailsHostType left, WorkspacesWorkspaceDetailsHostType right) => !(left == right); /// - public override bool Equals(object? obj) => obj is TaskClientOwnerPresence other && Equals(other); + public override bool Equals(object? obj) => obj is WorkspacesWorkspaceDetailsHostType other && Equals(other); /// - public bool Equals(TaskClientOwnerPresence other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + public bool Equals(WorkspacesWorkspaceDetailsHostType other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); /// public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); @@ -34350,74 +35299,68 @@ public TaskClientOwnerPresence(string value) /// public override string ToString() => Value; - /// Provides a for serializing instances. + /// Provides a for serializing instances. [EditorBrowsable(EditorBrowsableState.Never)] - public sealed class Converter : JsonConverter + public sealed class Converter : JsonConverter { /// - public override TaskClientOwnerPresence Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override WorkspacesWorkspaceDetailsHostType Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); } /// - public override void Write(Utf8JsonWriter writer, TaskClientOwnerPresence value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, WorkspacesWorkspaceDetailsHostType value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(TaskClientOwnerPresence)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(WorkspacesWorkspaceDetailsHostType)); } } } -/// Lifecycle status of a client-owned task. +/// Type of change represented by this file diff. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct TaskClientStatus : IEquatable +public readonly struct WorkspaceDiffFileChangeType : IEquatable { private readonly string? _value; - /// Initializes a new instance of the struct. - /// The value to associate with this . + /// Initializes a new instance of the struct. + /// The value to associate with this . [JsonConstructor] - public TaskClientStatus(string value) + public WorkspaceDiffFileChangeType(string value) { ArgumentException.ThrowIfNullOrWhiteSpace(value); _value = value; } - /// Gets the value associated with this . + /// Gets the value associated with this . public string Value => _value ?? string.Empty; - /// The external owner is actively working. - public static TaskClientStatus Running { get; } = new("running"); - - /// The external owner is connected but waiting. - public static TaskClientStatus Idle { get; } = new("idle"); - - /// The owner reported successful completion. - public static TaskClientStatus Completed { get; } = new("completed"); + /// The file was added. + public static WorkspaceDiffFileChangeType Added { get; } = new("added"); - /// The owner reported failure. - public static TaskClientStatus Failed { get; } = new("failed"); + /// The file was modified. + public static WorkspaceDiffFileChangeType Modified { get; } = new("modified"); - /// The owner reported or confirmed cancellation. - public static TaskClientStatus Cancelled { get; } = new("cancelled"); + /// The file was deleted. + public static WorkspaceDiffFileChangeType Deleted { get; } = new("deleted"); - /// The bound owner join disappeared; external executor state is unknown. - public static TaskClientStatus Orphaned { get; } = new("orphaned"); + /// The file was renamed. + public static WorkspaceDiffFileChangeType Renamed { get; } = new("renamed"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(TaskClientStatus left, TaskClientStatus right) => left.Equals(right); + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(WorkspaceDiffFileChangeType left, WorkspaceDiffFileChangeType right) => left.Equals(right); - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(TaskClientStatus left, TaskClientStatus right) => !(left == right); + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(WorkspaceDiffFileChangeType left, WorkspaceDiffFileChangeType right) => !(left == right); /// - public override bool Equals(object? obj) => obj is TaskClientStatus other && Equals(other); + public override bool Equals(object? obj) => obj is WorkspaceDiffFileChangeType other && Equals(other); /// - public bool Equals(TaskClientStatus other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + public bool Equals(WorkspaceDiffFileChangeType other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); /// public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); @@ -34425,62 +35368,65 @@ public TaskClientStatus(string value) /// public override string ToString() => Value; - /// Provides a for serializing instances. + /// Provides a for serializing instances. [EditorBrowsable(EditorBrowsableState.Never)] - public sealed class Converter : JsonConverter + public sealed class Converter : JsonConverter { /// - public override TaskClientStatus Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override WorkspaceDiffFileChangeType Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); } /// - public override void Write(Utf8JsonWriter writer, TaskClientStatus value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, WorkspaceDiffFileChangeType value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(TaskClientStatus)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(WorkspaceDiffFileChangeType)); } } } -/// Whether the shell runs inside a managed PTY session or as an independent background process. +/// Diff mode requested by the client. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct TaskShellInfoAttachmentMode : IEquatable +public readonly struct WorkspaceDiffMode : IEquatable { private readonly string? _value; - /// Initializes a new instance of the struct. - /// The value to associate with this . + /// Initializes a new instance of the struct. + /// The value to associate with this . [JsonConstructor] - public TaskShellInfoAttachmentMode(string value) + public WorkspaceDiffMode(string value) { ArgumentException.ThrowIfNullOrWhiteSpace(value); _value = value; } - /// Gets the value associated with this . + /// Gets the value associated with this . public string Value => _value ?? string.Empty; - /// The shell runs in a managed PTY session. - public static TaskShellInfoAttachmentMode Attached { get; } = new("attached"); + /// Return staged, unstaged, and untracked working tree changes. + public static WorkspaceDiffMode Unstaged { get; } = new("unstaged"); - /// The shell runs as an independent background process. - public static TaskShellInfoAttachmentMode Detached { get; } = new("detached"); + /// Return changes compared with the default branch. + public static WorkspaceDiffMode Branch { get; } = new("branch"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(TaskShellInfoAttachmentMode left, TaskShellInfoAttachmentMode right) => left.Equals(right); + /// Return the cumulative diff of files Copilot changed this session (used in non-git workspaces). + public static WorkspaceDiffMode Session { get; } = new("session"); - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(TaskShellInfoAttachmentMode left, TaskShellInfoAttachmentMode right) => !(left == right); + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(WorkspaceDiffMode left, WorkspaceDiffMode right) => left.Equals(right); + + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(WorkspaceDiffMode left, WorkspaceDiffMode right) => !(left == right); /// - public override bool Equals(object? obj) => obj is TaskShellInfoAttachmentMode other && Equals(other); + public override bool Equals(object? obj) => obj is WorkspaceDiffMode other && Equals(other); /// - public bool Equals(TaskShellInfoAttachmentMode other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + public bool Equals(WorkspaceDiffMode other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); /// public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); @@ -34488,59 +35434,65 @@ public TaskShellInfoAttachmentMode(string value) /// public override string ToString() => Value; - /// Provides a for serializing instances. + /// Provides a for serializing instances. [EditorBrowsable(EditorBrowsableState.Never)] - public sealed class Converter : JsonConverter + public sealed class Converter : JsonConverter { /// - public override TaskShellInfoAttachmentMode Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override WorkspaceDiffMode Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); } /// - public override void Write(Utf8JsonWriter writer, TaskShellInfoAttachmentMode value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, WorkspaceDiffMode value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(TaskShellInfoAttachmentMode)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(WorkspaceDiffMode)); } } } -/// Discriminator for a client-owned task. +/// Reason a rewind read (rewind points, file-restore preview, or session diff) could not be answered from the session's file-change captures. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct TaskClientType : IEquatable +public readonly struct HistoryRewindUnavailableReason : IEquatable { private readonly string? _value; - /// Initializes a new instance of the struct. - /// The value to associate with this . + /// Initializes a new instance of the struct. + /// The value to associate with this . [JsonConstructor] - public TaskClientType(string value) + public HistoryRewindUnavailableReason(string value) { ArgumentException.ThrowIfNullOrWhiteSpace(value); _value = value; } - /// Gets the value associated with this . + /// Gets the value associated with this . public string Value => _value ?? string.Empty; - /// Gets the client value. - public static TaskClientType Client { get; } = new("client"); + /// The session did not opt into file-change tracking before its first turn. + public static HistoryRewindUnavailableReason FileChangeTrackingDisabled { get; } = new("file-change-tracking-disabled"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(TaskClientType left, TaskClientType right) => left.Equals(right); + /// The session still has work that may mutate files or history. Transient: the same request succeeds once the session settles, so callers should retry rather than treat it as a failure. + public static HistoryRewindUnavailableReason SessionBusy { get; } = new("session-busy"); - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(TaskClientType left, TaskClientType right) => !(left == right); + /// Remote-backed rewind routing is not supported. + public static HistoryRewindUnavailableReason UnsupportedRemoteSession { get; } = new("unsupported-remote-session"); + + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(HistoryRewindUnavailableReason left, HistoryRewindUnavailableReason right) => left.Equals(right); + + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(HistoryRewindUnavailableReason left, HistoryRewindUnavailableReason right) => !(left == right); /// - public override bool Equals(object? obj) => obj is TaskClientType other && Equals(other); + public override bool Equals(object? obj) => obj is HistoryRewindUnavailableReason other && Equals(other); /// - public bool Equals(TaskClientType other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + public bool Equals(HistoryRewindUnavailableReason other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); /// public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); @@ -34548,62 +35500,65 @@ public TaskClientType(string value) /// public override string ToString() => Value; - /// Provides a for serializing instances. + /// Provides a for serializing instances. [EditorBrowsable(EditorBrowsableState.Never)] - public sealed class Converter : JsonConverter + public sealed class Converter : JsonConverter { /// - public override TaskClientType Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override HistoryRewindUnavailableReason Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); } /// - public override void Write(Utf8JsonWriter writer, TaskClientType value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, HistoryRewindUnavailableReason value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(TaskClientType)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(HistoryRewindUnavailableReason)); } } } -/// Active status a client owner may publish with a progress update. +/// Current normalized autopilot objective lifecycle status. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct TaskClientActiveStatus : IEquatable +public readonly struct AutopilotObjectiveStatus : IEquatable { private readonly string? _value; - /// Initializes a new instance of the struct. - /// The value to associate with this . + /// Initializes a new instance of the struct. + /// The value to associate with this . [JsonConstructor] - public TaskClientActiveStatus(string value) + public AutopilotObjectiveStatus(string value) { ArgumentException.ThrowIfNullOrWhiteSpace(value); _value = value; } - /// Gets the value associated with this . + /// Gets the value associated with this . public string Value => _value ?? string.Empty; - /// The external owner is actively working. - public static TaskClientActiveStatus Running { get; } = new("running"); + /// The objective is actively running. + public static AutopilotObjectiveStatus Active { get; } = new("active"); - /// The external owner is connected but waiting. - public static TaskClientActiveStatus Idle { get; } = new("idle"); + /// The objective is paused and may be resumed. + public static AutopilotObjectiveStatus Paused { get; } = new("paused"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(TaskClientActiveStatus left, TaskClientActiveStatus right) => left.Equals(right); + /// The objective completed. + public static AutopilotObjectiveStatus Completed { get; } = new("completed"); - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(TaskClientActiveStatus left, TaskClientActiveStatus right) => !(left == right); + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(AutopilotObjectiveStatus left, AutopilotObjectiveStatus right) => left.Equals(right); + + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(AutopilotObjectiveStatus left, AutopilotObjectiveStatus right) => !(left == right); /// - public override bool Equals(object? obj) => obj is TaskClientActiveStatus other && Equals(other); + public override bool Equals(object? obj) => obj is AutopilotObjectiveStatus other && Equals(other); /// - public bool Equals(TaskClientActiveStatus other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + public bool Equals(AutopilotObjectiveStatus other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); /// public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); @@ -34611,62 +35566,65 @@ public TaskClientActiveStatus(string value) /// public override string ToString() => Value; - /// Provides a for serializing instances. + /// Provides a for serializing instances. [EditorBrowsable(EditorBrowsableState.Never)] - public sealed class Converter : JsonConverter + public sealed class Converter : JsonConverter { /// - public override TaskClientActiveStatus Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override AutopilotObjectiveStatus Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); } /// - public override void Write(Utf8JsonWriter writer, TaskClientActiveStatus value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, AutopilotObjectiveStatus value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(TaskClientActiveStatus)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(AutopilotObjectiveStatus)); } } } -/// Consumer allowed to call an MCP tool. +/// Result of reloading a customization component. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct McpToolUiVisibility : IEquatable +public readonly struct CustomizationReloadStatus : IEquatable { private readonly string? _value; - /// Initializes a new instance of the struct. - /// The value to associate with this . + /// Initializes a new instance of the struct. + /// The value to associate with this . [JsonConstructor] - public McpToolUiVisibility(string value) + public CustomizationReloadStatus(string value) { ArgumentException.ThrowIfNullOrWhiteSpace(value); _value = value; } - /// Gets the value associated with this . + /// Gets the value associated with this . public string Value => _value ?? string.Empty; - /// The model may call the tool. - public static McpToolUiVisibility Model { get; } = new("model"); + /// The component was refreshed successfully. + public static CustomizationReloadStatus Reloaded { get; } = new("reloaded"); - /// An MCP App view may call the tool. - public static McpToolUiVisibility App { get; } = new("app"); + /// The component was not configured, loaded, or eligible for refresh. + public static CustomizationReloadStatus Skipped { get; } = new("skipped"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(McpToolUiVisibility left, McpToolUiVisibility right) => left.Equals(right); + /// The component could not be refreshed; other components may still reload. + public static CustomizationReloadStatus Failed { get; } = new("failed"); - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(McpToolUiVisibility left, McpToolUiVisibility right) => !(left == right); + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(CustomizationReloadStatus left, CustomizationReloadStatus right) => left.Equals(right); + + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(CustomizationReloadStatus left, CustomizationReloadStatus right) => !(left == right); /// - public override bool Equals(object? obj) => obj is McpToolUiVisibility other && Equals(other); + public override bool Equals(object? obj) => obj is CustomizationReloadStatus other && Equals(other); /// - public bool Equals(McpToolUiVisibility other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + public bool Equals(CustomizationReloadStatus other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); /// public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); @@ -34674,65 +35632,80 @@ public McpToolUiVisibility(string value) /// public override string ToString() => Value; - /// Provides a for serializing instances. + /// Provides a for serializing instances. [EditorBrowsable(EditorBrowsableState.Never)] - public sealed class Converter : JsonConverter + public sealed class Converter : JsonConverter { /// - public override McpToolUiVisibility Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override CustomizationReloadStatus Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); } /// - public override void Write(Utf8JsonWriter writer, McpToolUiVisibility value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, CustomizationReloadStatus value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(McpToolUiVisibility)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(CustomizationReloadStatus)); } } } -/// Outcome of the sampling inference. 'success' produced a response; 'failure' encountered an error (including agent-side rejection by content filter or criteria); 'cancelled' the caller cancelled this execution via cancelSamplingExecution. +/// Component of session customization discovery. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct McpSamplingExecutionAction : IEquatable +public readonly struct CustomizationReloadSubsystem : IEquatable { private readonly string? _value; - /// Initializes a new instance of the struct. - /// The value to associate with this . + /// Initializes a new instance of the struct. + /// The value to associate with this . [JsonConstructor] - public McpSamplingExecutionAction(string value) + public CustomizationReloadSubsystem(string value) { ArgumentException.ThrowIfNullOrWhiteSpace(value); _value = value; } - /// Gets the value associated with this . + /// Gets the value associated with this . public string Value => _value ?? string.Empty; - /// The sampling inference completed and produced a result. - public static McpSamplingExecutionAction Success { get; } = new("success"); + /// Repository metadata and working-directory context. + public static CustomizationReloadSubsystem RepositoryContext { get; } = new("repositoryContext"); - /// The sampling inference failed or was rejected. - public static McpSamplingExecutionAction Failure { get; } = new("failure"); + /// Session instructions and their cached dynamic context. + public static CustomizationReloadSubsystem Instructions { get; } = new("instructions"); - /// The sampling inference was cancelled before completion. - public static McpSamplingExecutionAction Cancelled { get; } = new("cancelled"); + /// Discovered plugin configuration. + public static CustomizationReloadSubsystem Plugins { get; } = new("plugins"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(McpSamplingExecutionAction left, McpSamplingExecutionAction right) => left.Equals(right); + /// Configured session and plugin hooks. + public static CustomizationReloadSubsystem Hooks { get; } = new("hooks"); - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(McpSamplingExecutionAction left, McpSamplingExecutionAction right) => !(left == right); + /// Discovered skills. + public static CustomizationReloadSubsystem Skills { get; } = new("skills"); + + /// Discovered custom agents. + public static CustomizationReloadSubsystem Agents { get; } = new("agents"); + + /// Loaded MCP server configuration. + public static CustomizationReloadSubsystem Mcp { get; } = new("mcp"); + + /// Configured session extensions. + public static CustomizationReloadSubsystem Extensions { get; } = new("extensions"); + + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(CustomizationReloadSubsystem left, CustomizationReloadSubsystem right) => left.Equals(right); + + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(CustomizationReloadSubsystem left, CustomizationReloadSubsystem right) => !(left == right); /// - public override bool Equals(object? obj) => obj is McpSamplingExecutionAction other && Equals(other); + public override bool Equals(object? obj) => obj is CustomizationReloadSubsystem other && Equals(other); /// - public bool Equals(McpSamplingExecutionAction other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + public bool Equals(CustomizationReloadSubsystem other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); /// public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); @@ -34740,62 +35713,62 @@ public McpSamplingExecutionAction(string value) /// public override string ToString() => Value; - /// Provides a for serializing instances. + /// Provides a for serializing instances. [EditorBrowsable(EditorBrowsableState.Never)] - public sealed class Converter : JsonConverter + public sealed class Converter : JsonConverter { /// - public override McpSamplingExecutionAction Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override CustomizationReloadSubsystem Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); } /// - public override void Write(Utf8JsonWriter writer, McpSamplingExecutionAction value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, CustomizationReloadSubsystem value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(McpSamplingExecutionAction)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(CustomizationReloadSubsystem)); } } } -/// How environment-variable values supplied to MCP servers are resolved. "direct" passes literal string values; "indirect" treats values as references (e.g. names of environment variables on the host) that the runtime resolves before launch. Defaults to the runtime's startup mode; clients that intentionally launch MCP servers with literal values (e.g. CLI prompt mode and ACP) set this to "direct". +/// Whether task execution is synchronously awaited or managed in the background. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct McpSetEnvValueModeDetails : IEquatable +public readonly struct TaskExecutionMode : IEquatable { private readonly string? _value; - /// Initializes a new instance of the struct. - /// The value to associate with this . + /// Initializes a new instance of the struct. + /// The value to associate with this . [JsonConstructor] - public McpSetEnvValueModeDetails(string value) + public TaskExecutionMode(string value) { ArgumentException.ThrowIfNullOrWhiteSpace(value); _value = value; } - /// Gets the value associated with this . + /// Gets the value associated with this . public string Value => _value ?? string.Empty; - /// Treat MCP server environment values as literal strings. - public static McpSetEnvValueModeDetails Direct { get; } = new("direct"); + /// The task was started with synchronous waiting. + public static TaskExecutionMode Sync { get; } = new("sync"); - /// Treat MCP server environment values as host-side references to resolve before launch. - public static McpSetEnvValueModeDetails Indirect { get; } = new("indirect"); + /// The task is managed in the background. + public static TaskExecutionMode Background { get; } = new("background"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(McpSetEnvValueModeDetails left, McpSetEnvValueModeDetails right) => left.Equals(right); + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(TaskExecutionMode left, TaskExecutionMode right) => left.Equals(right); - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(McpSetEnvValueModeDetails left, McpSetEnvValueModeDetails right) => !(left == right); + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(TaskExecutionMode left, TaskExecutionMode right) => !(left == right); /// - public override bool Equals(object? obj) => obj is McpSetEnvValueModeDetails other && Equals(other); + public override bool Equals(object? obj) => obj is TaskExecutionMode other && Equals(other); /// - public bool Equals(McpSetEnvValueModeDetails other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + public bool Equals(TaskExecutionMode other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); /// public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); @@ -34803,62 +35776,71 @@ public McpSetEnvValueModeDetails(string value) /// public override string ToString() => Value; - /// Provides a for serializing instances. + /// Provides a for serializing instances. [EditorBrowsable(EditorBrowsableState.Never)] - public sealed class Converter : JsonConverter + public sealed class Converter : JsonConverter { /// - public override McpSetEnvValueModeDetails Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override TaskExecutionMode Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); } /// - public override void Write(Utf8JsonWriter writer, McpSetEnvValueModeDetails value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, TaskExecutionMode value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(McpSetEnvValueModeDetails)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(TaskExecutionMode)); } } } -/// Outcome of starting the original prepared owned login. +/// Current lifecycle status of the task. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct McpOwnedOauthLoginStatus : IEquatable +public readonly struct TaskStatus : IEquatable { private readonly string? _value; - /// Initializes a new instance of the struct. - /// The value to associate with this . + /// Initializes a new instance of the struct. + /// The value to associate with this . [JsonConstructor] - public McpOwnedOauthLoginStatus(string value) + public TaskStatus(string value) { ArgumentException.ThrowIfNullOrWhiteSpace(value); _value = value; } - /// Gets the value associated with this . + /// Gets the value associated with this . public string Value => _value ?? string.Empty; - /// The original requester may open the returned authorisation URL. - public static McpOwnedOauthLoginStatus AwaitingBrowser { get; } = new("awaiting-browser"); + /// The task is actively executing. + public static TaskStatus Running { get; } = new("running"); - /// Cached credentials were accepted and the original server finished reconnecting. - public static McpOwnedOauthLoginStatus Connected { get; } = new("connected"); + /// The task is waiting for additional input. + public static TaskStatus Idle { get; } = new("idle"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(McpOwnedOauthLoginStatus left, McpOwnedOauthLoginStatus right) => left.Equals(right); + /// The task finished successfully. + public static TaskStatus Completed { get; } = new("completed"); - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(McpOwnedOauthLoginStatus left, McpOwnedOauthLoginStatus right) => !(left == right); + /// The task finished with an error. + public static TaskStatus Failed { get; } = new("failed"); + + /// The task was cancelled before completion. + public static TaskStatus Cancelled { get; } = new("cancelled"); + + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(TaskStatus left, TaskStatus right) => left.Equals(right); + + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(TaskStatus left, TaskStatus right) => !(left == right); /// - public override bool Equals(object? obj) => obj is McpOwnedOauthLoginStatus other && Equals(other); + public override bool Equals(object? obj) => obj is TaskStatus other && Equals(other); /// - public bool Equals(McpOwnedOauthLoginStatus other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + public bool Equals(TaskStatus other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); /// public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); @@ -34866,62 +35848,59 @@ public McpOwnedOauthLoginStatus(string value) /// public override string ToString() => Value; - /// Provides a for serializing instances. + /// Provides a for serializing instances. [EditorBrowsable(EditorBrowsableState.Never)] - public sealed class Converter : JsonConverter + public sealed class Converter : JsonConverter { /// - public override McpOwnedOauthLoginStatus Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override TaskStatus Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); } /// - public override void Write(Utf8JsonWriter writer, McpOwnedOauthLoginStatus value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, TaskStatus value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(McpOwnedOauthLoginStatus)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(TaskStatus)); } } } -/// OAuth grant type override for this login. +/// Client-owned tasks always execute outside the runtime in background mode. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct McpOauthLoginGrantType : IEquatable +public readonly struct TaskClientExecutionMode : IEquatable { private readonly string? _value; - /// Initializes a new instance of the struct. - /// The value to associate with this . + /// Initializes a new instance of the struct. + /// The value to associate with this . [JsonConstructor] - public McpOauthLoginGrantType(string value) + public TaskClientExecutionMode(string value) { ArgumentException.ThrowIfNullOrWhiteSpace(value); _value = value; } - /// Gets the value associated with this . + /// Gets the value associated with this . public string Value => _value ?? string.Empty; - /// Interactive browser-based OAuth flow using an authorization code, typically with PKCE. - public static McpOauthLoginGrantType AuthorizationCode { get; } = new("authorization_code"); - - /// Headless OAuth flow where a confidential client authenticates directly with a client secret. - public static McpOauthLoginGrantType ClientCredentials { get; } = new("client_credentials"); + /// Gets the background value. + public static TaskClientExecutionMode Background { get; } = new("background"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(McpOauthLoginGrantType left, McpOauthLoginGrantType right) => left.Equals(right); + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(TaskClientExecutionMode left, TaskClientExecutionMode right) => left.Equals(right); - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(McpOauthLoginGrantType left, McpOauthLoginGrantType right) => !(left == right); + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(TaskClientExecutionMode left, TaskClientExecutionMode right) => !(left == right); /// - public override bool Equals(object? obj) => obj is McpOauthLoginGrantType other && Equals(other); + public override bool Equals(object? obj) => obj is TaskClientExecutionMode other && Equals(other); /// - public bool Equals(McpOauthLoginGrantType other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + public bool Equals(TaskClientExecutionMode other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); /// public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); @@ -34929,65 +35908,62 @@ public McpOauthLoginGrantType(string value) /// public override string ToString() => Value; - /// Provides a for serializing instances. + /// Provides a for serializing instances. [EditorBrowsable(EditorBrowsableState.Never)] - public sealed class Converter : JsonConverter + public sealed class Converter : JsonConverter { /// - public override McpOauthLoginGrantType Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override TaskClientExecutionMode Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); } /// - public override void Write(Utf8JsonWriter writer, McpOauthLoginGrantType value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, TaskClientExecutionMode value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(McpOauthLoginGrantType)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(TaskClientExecutionMode)); } } } -/// Why a passive MCP OAuth probe determined authentication is needed. +/// Connection class owning a client task. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct McpOauthProbeNeedsAuthReason : IEquatable +public readonly struct TaskClientOwnerKind : IEquatable { private readonly string? _value; - /// Initializes a new instance of the struct. - /// The value to associate with this . + /// Initializes a new instance of the struct. + /// The value to associate with this . [JsonConstructor] - public McpOauthProbeNeedsAuthReason(string value) + public TaskClientOwnerKind(string value) { ArgumentException.ThrowIfNullOrWhiteSpace(value); _value = value; } - /// Gets the value associated with this . + /// Gets the value associated with this . public string Value => _value ?? string.Empty; - /// No token was sent and the server requires authentication. - public static McpOauthProbeNeedsAuthReason Initial { get; } = new("initial"); - - /// A cached token was sent and rejected. - public static McpOauthProbeNeedsAuthReason Refresh { get; } = new("refresh"); + /// A discovered extension connection owns the task. + public static TaskClientOwnerKind Extension { get; } = new("extension"); - /// The server returned a 403 insufficient_scope challenge, indicating additional scopes or audience are needed. - public static McpOauthProbeNeedsAuthReason Upscope { get; } = new("upscope"); + /// A generic SDK connection owns the task. + public static TaskClientOwnerKind Sdk { get; } = new("sdk"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(McpOauthProbeNeedsAuthReason left, McpOauthProbeNeedsAuthReason right) => left.Equals(right); + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(TaskClientOwnerKind left, TaskClientOwnerKind right) => left.Equals(right); - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(McpOauthProbeNeedsAuthReason left, McpOauthProbeNeedsAuthReason right) => !(left == right); + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(TaskClientOwnerKind left, TaskClientOwnerKind right) => !(left == right); /// - public override bool Equals(object? obj) => obj is McpOauthProbeNeedsAuthReason other && Equals(other); + public override bool Equals(object? obj) => obj is TaskClientOwnerKind other && Equals(other); /// - public bool Equals(McpOauthProbeNeedsAuthReason other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + public bool Equals(TaskClientOwnerKind other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); /// public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); @@ -34995,65 +35971,62 @@ public McpOauthProbeNeedsAuthReason(string value) /// public override string ToString() => Value; - /// Provides a for serializing instances. + /// Provides a for serializing instances. [EditorBrowsable(EditorBrowsableState.Never)] - public sealed class Converter : JsonConverter + public sealed class Converter : JsonConverter { /// - public override McpOauthProbeNeedsAuthReason Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override TaskClientOwnerKind Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); } /// - public override void Write(Utf8JsonWriter writer, McpOauthProbeNeedsAuthReason value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, TaskClientOwnerKind value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(McpOauthProbeNeedsAuthReason)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(TaskClientOwnerKind)); } } } -/// Allowed values for the `McpAppsSetHostContextDetailsAvailableDisplayMode` enumeration. +/// Presence of the task's bound join. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct McpAppsSetHostContextDetailsAvailableDisplayMode : IEquatable +public readonly struct TaskClientOwnerPresence : IEquatable { private readonly string? _value; - /// Initializes a new instance of the struct. - /// The value to associate with this . + /// Initializes a new instance of the struct. + /// The value to associate with this . [JsonConstructor] - public McpAppsSetHostContextDetailsAvailableDisplayMode(string value) + public TaskClientOwnerPresence(string value) { ArgumentException.ThrowIfNullOrWhiteSpace(value); _value = value; } - /// Gets the value associated with this . + /// Gets the value associated with this . public string Value => _value ?? string.Empty; - /// Rendered inline within the host conversation surface. - public static McpAppsSetHostContextDetailsAvailableDisplayMode Inline { get; } = new("inline"); - - /// Rendered as a fullscreen overlay. - public static McpAppsSetHostContextDetailsAvailableDisplayMode Fullscreen { get; } = new("fullscreen"); + /// The bound session join is connected. + public static TaskClientOwnerPresence Connected { get; } = new("connected"); - /// Rendered as a picture-in-picture floating panel. - public static McpAppsSetHostContextDetailsAvailableDisplayMode Pip { get; } = new("pip"); + /// The bound session join is disconnected. + public static TaskClientOwnerPresence Disconnected { get; } = new("disconnected"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(McpAppsSetHostContextDetailsAvailableDisplayMode left, McpAppsSetHostContextDetailsAvailableDisplayMode right) => left.Equals(right); + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(TaskClientOwnerPresence left, TaskClientOwnerPresence right) => left.Equals(right); - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(McpAppsSetHostContextDetailsAvailableDisplayMode left, McpAppsSetHostContextDetailsAvailableDisplayMode right) => !(left == right); + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(TaskClientOwnerPresence left, TaskClientOwnerPresence right) => !(left == right); /// - public override bool Equals(object? obj) => obj is McpAppsSetHostContextDetailsAvailableDisplayMode other && Equals(other); + public override bool Equals(object? obj) => obj is TaskClientOwnerPresence other && Equals(other); /// - public bool Equals(McpAppsSetHostContextDetailsAvailableDisplayMode other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + public bool Equals(TaskClientOwnerPresence other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); /// public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); @@ -35061,65 +36034,74 @@ public McpAppsSetHostContextDetailsAvailableDisplayMode(string value) /// public override string ToString() => Value; - /// Provides a for serializing instances. + /// Provides a for serializing instances. [EditorBrowsable(EditorBrowsableState.Never)] - public sealed class Converter : JsonConverter + public sealed class Converter : JsonConverter { /// - public override McpAppsSetHostContextDetailsAvailableDisplayMode Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override TaskClientOwnerPresence Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); } /// - public override void Write(Utf8JsonWriter writer, McpAppsSetHostContextDetailsAvailableDisplayMode value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, TaskClientOwnerPresence value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(McpAppsSetHostContextDetailsAvailableDisplayMode)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(TaskClientOwnerPresence)); } } } -/// Current display mode (SEP-1865). +/// Lifecycle status of a client-owned task. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct McpAppsSetHostContextDetailsDisplayMode : IEquatable +public readonly struct TaskClientStatus : IEquatable { private readonly string? _value; - /// Initializes a new instance of the struct. - /// The value to associate with this . + /// Initializes a new instance of the struct. + /// The value to associate with this . [JsonConstructor] - public McpAppsSetHostContextDetailsDisplayMode(string value) + public TaskClientStatus(string value) { ArgumentException.ThrowIfNullOrWhiteSpace(value); _value = value; } - /// Gets the value associated with this . + /// Gets the value associated with this . public string Value => _value ?? string.Empty; - /// Rendered inline within the host conversation surface. - public static McpAppsSetHostContextDetailsDisplayMode Inline { get; } = new("inline"); + /// The external owner is actively working. + public static TaskClientStatus Running { get; } = new("running"); - /// Rendered as a fullscreen overlay. - public static McpAppsSetHostContextDetailsDisplayMode Fullscreen { get; } = new("fullscreen"); + /// The external owner is connected but waiting. + public static TaskClientStatus Idle { get; } = new("idle"); - /// Rendered as a picture-in-picture floating panel. - public static McpAppsSetHostContextDetailsDisplayMode Pip { get; } = new("pip"); + /// The owner reported successful completion. + public static TaskClientStatus Completed { get; } = new("completed"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(McpAppsSetHostContextDetailsDisplayMode left, McpAppsSetHostContextDetailsDisplayMode right) => left.Equals(right); + /// The owner reported failure. + public static TaskClientStatus Failed { get; } = new("failed"); - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(McpAppsSetHostContextDetailsDisplayMode left, McpAppsSetHostContextDetailsDisplayMode right) => !(left == right); + /// The owner reported or confirmed cancellation. + public static TaskClientStatus Cancelled { get; } = new("cancelled"); + + /// The bound owner join disappeared; external executor state is unknown. + public static TaskClientStatus Orphaned { get; } = new("orphaned"); + + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(TaskClientStatus left, TaskClientStatus right) => left.Equals(right); + + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(TaskClientStatus left, TaskClientStatus right) => !(left == right); /// - public override bool Equals(object? obj) => obj is McpAppsSetHostContextDetailsDisplayMode other && Equals(other); + public override bool Equals(object? obj) => obj is TaskClientStatus other && Equals(other); /// - public bool Equals(McpAppsSetHostContextDetailsDisplayMode other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + public bool Equals(TaskClientStatus other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); /// public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); @@ -35127,65 +36109,62 @@ public McpAppsSetHostContextDetailsDisplayMode(string value) /// public override string ToString() => Value; - /// Provides a for serializing instances. + /// Provides a for serializing instances. [EditorBrowsable(EditorBrowsableState.Never)] - public sealed class Converter : JsonConverter + public sealed class Converter : JsonConverter { /// - public override McpAppsSetHostContextDetailsDisplayMode Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override TaskClientStatus Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); } /// - public override void Write(Utf8JsonWriter writer, McpAppsSetHostContextDetailsDisplayMode value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, TaskClientStatus value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(McpAppsSetHostContextDetailsDisplayMode)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(TaskClientStatus)); } } } -/// Platform type for responsive design. +/// Whether the shell runs inside a managed PTY session or as an independent background process. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct McpAppsSetHostContextDetailsPlatform : IEquatable +public readonly struct TaskShellInfoAttachmentMode : IEquatable { private readonly string? _value; - /// Initializes a new instance of the struct. - /// The value to associate with this . + /// Initializes a new instance of the struct. + /// The value to associate with this . [JsonConstructor] - public McpAppsSetHostContextDetailsPlatform(string value) + public TaskShellInfoAttachmentMode(string value) { ArgumentException.ThrowIfNullOrWhiteSpace(value); _value = value; } - /// Gets the value associated with this . + /// Gets the value associated with this . public string Value => _value ?? string.Empty; - /// Host runs in a web browser. - public static McpAppsSetHostContextDetailsPlatform Web { get; } = new("web"); - - /// Host runs as a desktop application. - public static McpAppsSetHostContextDetailsPlatform Desktop { get; } = new("desktop"); + /// The shell runs in a managed PTY session. + public static TaskShellInfoAttachmentMode Attached { get; } = new("attached"); - /// Host runs on a mobile device. - public static McpAppsSetHostContextDetailsPlatform Mobile { get; } = new("mobile"); + /// The shell runs as an independent background process. + public static TaskShellInfoAttachmentMode Detached { get; } = new("detached"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(McpAppsSetHostContextDetailsPlatform left, McpAppsSetHostContextDetailsPlatform right) => left.Equals(right); + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(TaskShellInfoAttachmentMode left, TaskShellInfoAttachmentMode right) => left.Equals(right); - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(McpAppsSetHostContextDetailsPlatform left, McpAppsSetHostContextDetailsPlatform right) => !(left == right); + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(TaskShellInfoAttachmentMode left, TaskShellInfoAttachmentMode right) => !(left == right); /// - public override bool Equals(object? obj) => obj is McpAppsSetHostContextDetailsPlatform other && Equals(other); + public override bool Equals(object? obj) => obj is TaskShellInfoAttachmentMode other && Equals(other); /// - public bool Equals(McpAppsSetHostContextDetailsPlatform other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + public bool Equals(TaskShellInfoAttachmentMode other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); /// public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); @@ -35193,62 +36172,59 @@ public McpAppsSetHostContextDetailsPlatform(string value) /// public override string ToString() => Value; - /// Provides a for serializing instances. + /// Provides a for serializing instances. [EditorBrowsable(EditorBrowsableState.Never)] - public sealed class Converter : JsonConverter + public sealed class Converter : JsonConverter { /// - public override McpAppsSetHostContextDetailsPlatform Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override TaskShellInfoAttachmentMode Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); } /// - public override void Write(Utf8JsonWriter writer, McpAppsSetHostContextDetailsPlatform value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, TaskShellInfoAttachmentMode value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(McpAppsSetHostContextDetailsPlatform)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(TaskShellInfoAttachmentMode)); } } } -/// UI theme preference per SEP-1865. +/// Discriminator for a client-owned task. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct McpAppsSetHostContextDetailsTheme : IEquatable +public readonly struct TaskClientType : IEquatable { private readonly string? _value; - /// Initializes a new instance of the struct. - /// The value to associate with this . + /// Initializes a new instance of the struct. + /// The value to associate with this . [JsonConstructor] - public McpAppsSetHostContextDetailsTheme(string value) + public TaskClientType(string value) { ArgumentException.ThrowIfNullOrWhiteSpace(value); _value = value; } - /// Gets the value associated with this . + /// Gets the value associated with this . public string Value => _value ?? string.Empty; - /// Light UI theme. - public static McpAppsSetHostContextDetailsTheme Light { get; } = new("light"); - - /// Dark UI theme. - public static McpAppsSetHostContextDetailsTheme Dark { get; } = new("dark"); + /// Gets the client value. + public static TaskClientType Client { get; } = new("client"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(McpAppsSetHostContextDetailsTheme left, McpAppsSetHostContextDetailsTheme right) => left.Equals(right); + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(TaskClientType left, TaskClientType right) => left.Equals(right); - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(McpAppsSetHostContextDetailsTheme left, McpAppsSetHostContextDetailsTheme right) => !(left == right); + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(TaskClientType left, TaskClientType right) => !(left == right); /// - public override bool Equals(object? obj) => obj is McpAppsSetHostContextDetailsTheme other && Equals(other); + public override bool Equals(object? obj) => obj is TaskClientType other && Equals(other); /// - public bool Equals(McpAppsSetHostContextDetailsTheme other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + public bool Equals(TaskClientType other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); /// public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); @@ -35256,65 +36232,62 @@ public McpAppsSetHostContextDetailsTheme(string value) /// public override string ToString() => Value; - /// Provides a for serializing instances. + /// Provides a for serializing instances. [EditorBrowsable(EditorBrowsableState.Never)] - public sealed class Converter : JsonConverter + public sealed class Converter : JsonConverter { /// - public override McpAppsSetHostContextDetailsTheme Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override TaskClientType Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); } /// - public override void Write(Utf8JsonWriter writer, McpAppsSetHostContextDetailsTheme value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, TaskClientType value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(McpAppsSetHostContextDetailsTheme)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(TaskClientType)); } } } -/// Allowed values for the `McpAppsHostContextDetailsAvailableDisplayMode` enumeration. +/// Active status a client owner may publish with a progress update. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct McpAppsHostContextDetailsAvailableDisplayMode : IEquatable +public readonly struct TaskClientActiveStatus : IEquatable { private readonly string? _value; - /// Initializes a new instance of the struct. - /// The value to associate with this . + /// Initializes a new instance of the struct. + /// The value to associate with this . [JsonConstructor] - public McpAppsHostContextDetailsAvailableDisplayMode(string value) + public TaskClientActiveStatus(string value) { ArgumentException.ThrowIfNullOrWhiteSpace(value); _value = value; } - /// Gets the value associated with this . + /// Gets the value associated with this . public string Value => _value ?? string.Empty; - /// Rendered inline within the host conversation surface. - public static McpAppsHostContextDetailsAvailableDisplayMode Inline { get; } = new("inline"); - - /// Rendered as a fullscreen overlay. - public static McpAppsHostContextDetailsAvailableDisplayMode Fullscreen { get; } = new("fullscreen"); + /// The external owner is actively working. + public static TaskClientActiveStatus Running { get; } = new("running"); - /// Rendered as a picture-in-picture floating panel. - public static McpAppsHostContextDetailsAvailableDisplayMode Pip { get; } = new("pip"); + /// The external owner is connected but waiting. + public static TaskClientActiveStatus Idle { get; } = new("idle"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(McpAppsHostContextDetailsAvailableDisplayMode left, McpAppsHostContextDetailsAvailableDisplayMode right) => left.Equals(right); + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(TaskClientActiveStatus left, TaskClientActiveStatus right) => left.Equals(right); - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(McpAppsHostContextDetailsAvailableDisplayMode left, McpAppsHostContextDetailsAvailableDisplayMode right) => !(left == right); + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(TaskClientActiveStatus left, TaskClientActiveStatus right) => !(left == right); /// - public override bool Equals(object? obj) => obj is McpAppsHostContextDetailsAvailableDisplayMode other && Equals(other); + public override bool Equals(object? obj) => obj is TaskClientActiveStatus other && Equals(other); /// - public bool Equals(McpAppsHostContextDetailsAvailableDisplayMode other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + public bool Equals(TaskClientActiveStatus other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); /// public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); @@ -35322,65 +36295,62 @@ public McpAppsHostContextDetailsAvailableDisplayMode(string value) /// public override string ToString() => Value; - /// Provides a for serializing instances. + /// Provides a for serializing instances. [EditorBrowsable(EditorBrowsableState.Never)] - public sealed class Converter : JsonConverter + public sealed class Converter : JsonConverter { /// - public override McpAppsHostContextDetailsAvailableDisplayMode Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override TaskClientActiveStatus Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); } /// - public override void Write(Utf8JsonWriter writer, McpAppsHostContextDetailsAvailableDisplayMode value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, TaskClientActiveStatus value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(McpAppsHostContextDetailsAvailableDisplayMode)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(TaskClientActiveStatus)); } } } -/// Current display mode (SEP-1865). +/// Consumer allowed to call an MCP tool. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct McpAppsHostContextDetailsDisplayMode : IEquatable +public readonly struct McpToolUiVisibility : IEquatable { private readonly string? _value; - /// Initializes a new instance of the struct. - /// The value to associate with this . + /// Initializes a new instance of the struct. + /// The value to associate with this . [JsonConstructor] - public McpAppsHostContextDetailsDisplayMode(string value) + public McpToolUiVisibility(string value) { ArgumentException.ThrowIfNullOrWhiteSpace(value); _value = value; } - /// Gets the value associated with this . + /// Gets the value associated with this . public string Value => _value ?? string.Empty; - /// Rendered inline within the host conversation surface. - public static McpAppsHostContextDetailsDisplayMode Inline { get; } = new("inline"); - - /// Rendered as a fullscreen overlay. - public static McpAppsHostContextDetailsDisplayMode Fullscreen { get; } = new("fullscreen"); + /// The model may call the tool. + public static McpToolUiVisibility Model { get; } = new("model"); - /// Rendered as a picture-in-picture floating panel. - public static McpAppsHostContextDetailsDisplayMode Pip { get; } = new("pip"); + /// An MCP App view may call the tool. + public static McpToolUiVisibility App { get; } = new("app"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(McpAppsHostContextDetailsDisplayMode left, McpAppsHostContextDetailsDisplayMode right) => left.Equals(right); + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(McpToolUiVisibility left, McpToolUiVisibility right) => left.Equals(right); - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(McpAppsHostContextDetailsDisplayMode left, McpAppsHostContextDetailsDisplayMode right) => !(left == right); + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(McpToolUiVisibility left, McpToolUiVisibility right) => !(left == right); /// - public override bool Equals(object? obj) => obj is McpAppsHostContextDetailsDisplayMode other && Equals(other); + public override bool Equals(object? obj) => obj is McpToolUiVisibility other && Equals(other); /// - public bool Equals(McpAppsHostContextDetailsDisplayMode other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + public bool Equals(McpToolUiVisibility other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); /// public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); @@ -35388,65 +36358,65 @@ public McpAppsHostContextDetailsDisplayMode(string value) /// public override string ToString() => Value; - /// Provides a for serializing instances. + /// Provides a for serializing instances. [EditorBrowsable(EditorBrowsableState.Never)] - public sealed class Converter : JsonConverter + public sealed class Converter : JsonConverter { /// - public override McpAppsHostContextDetailsDisplayMode Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override McpToolUiVisibility Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); } /// - public override void Write(Utf8JsonWriter writer, McpAppsHostContextDetailsDisplayMode value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, McpToolUiVisibility value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(McpAppsHostContextDetailsDisplayMode)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(McpToolUiVisibility)); } } } -/// Platform type for responsive design. +/// Outcome of the sampling inference. 'success' produced a response; 'failure' encountered an error (including agent-side rejection by content filter or criteria); 'cancelled' the caller cancelled this execution via cancelSamplingExecution. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct McpAppsHostContextDetailsPlatform : IEquatable +public readonly struct McpSamplingExecutionAction : IEquatable { private readonly string? _value; - /// Initializes a new instance of the struct. - /// The value to associate with this . + /// Initializes a new instance of the struct. + /// The value to associate with this . [JsonConstructor] - public McpAppsHostContextDetailsPlatform(string value) + public McpSamplingExecutionAction(string value) { ArgumentException.ThrowIfNullOrWhiteSpace(value); _value = value; } - /// Gets the value associated with this . + /// Gets the value associated with this . public string Value => _value ?? string.Empty; - /// Host runs in a web browser. - public static McpAppsHostContextDetailsPlatform Web { get; } = new("web"); + /// The sampling inference completed and produced a result. + public static McpSamplingExecutionAction Success { get; } = new("success"); - /// Host runs as a desktop application. - public static McpAppsHostContextDetailsPlatform Desktop { get; } = new("desktop"); + /// The sampling inference failed or was rejected. + public static McpSamplingExecutionAction Failure { get; } = new("failure"); - /// Host runs on a mobile device. - public static McpAppsHostContextDetailsPlatform Mobile { get; } = new("mobile"); + /// The sampling inference was cancelled before completion. + public static McpSamplingExecutionAction Cancelled { get; } = new("cancelled"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(McpAppsHostContextDetailsPlatform left, McpAppsHostContextDetailsPlatform right) => left.Equals(right); + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(McpSamplingExecutionAction left, McpSamplingExecutionAction right) => left.Equals(right); - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(McpAppsHostContextDetailsPlatform left, McpAppsHostContextDetailsPlatform right) => !(left == right); + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(McpSamplingExecutionAction left, McpSamplingExecutionAction right) => !(left == right); /// - public override bool Equals(object? obj) => obj is McpAppsHostContextDetailsPlatform other && Equals(other); + public override bool Equals(object? obj) => obj is McpSamplingExecutionAction other && Equals(other); /// - public bool Equals(McpAppsHostContextDetailsPlatform other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + public bool Equals(McpSamplingExecutionAction other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); /// public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); @@ -35454,62 +36424,62 @@ public McpAppsHostContextDetailsPlatform(string value) /// public override string ToString() => Value; - /// Provides a for serializing instances. + /// Provides a for serializing instances. [EditorBrowsable(EditorBrowsableState.Never)] - public sealed class Converter : JsonConverter + public sealed class Converter : JsonConverter { /// - public override McpAppsHostContextDetailsPlatform Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override McpSamplingExecutionAction Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); } /// - public override void Write(Utf8JsonWriter writer, McpAppsHostContextDetailsPlatform value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, McpSamplingExecutionAction value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(McpAppsHostContextDetailsPlatform)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(McpSamplingExecutionAction)); } } } -/// UI theme preference per SEP-1865. +/// How environment-variable values supplied to MCP servers are resolved. "direct" passes literal string values; "indirect" treats values as references (e.g. names of environment variables on the host) that the runtime resolves before launch. Defaults to the runtime's startup mode; clients that intentionally launch MCP servers with literal values (e.g. CLI prompt mode and ACP) set this to "direct". [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct McpAppsHostContextDetailsTheme : IEquatable +public readonly struct McpSetEnvValueModeDetails : IEquatable { private readonly string? _value; - /// Initializes a new instance of the struct. - /// The value to associate with this . + /// Initializes a new instance of the struct. + /// The value to associate with this . [JsonConstructor] - public McpAppsHostContextDetailsTheme(string value) + public McpSetEnvValueModeDetails(string value) { ArgumentException.ThrowIfNullOrWhiteSpace(value); _value = value; } - /// Gets the value associated with this . + /// Gets the value associated with this . public string Value => _value ?? string.Empty; - /// Light UI theme. - public static McpAppsHostContextDetailsTheme Light { get; } = new("light"); + /// Treat MCP server environment values as literal strings. + public static McpSetEnvValueModeDetails Direct { get; } = new("direct"); - /// Dark UI theme. - public static McpAppsHostContextDetailsTheme Dark { get; } = new("dark"); + /// Treat MCP server environment values as host-side references to resolve before launch. + public static McpSetEnvValueModeDetails Indirect { get; } = new("indirect"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(McpAppsHostContextDetailsTheme left, McpAppsHostContextDetailsTheme right) => left.Equals(right); + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(McpSetEnvValueModeDetails left, McpSetEnvValueModeDetails right) => left.Equals(right); - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(McpAppsHostContextDetailsTheme left, McpAppsHostContextDetailsTheme right) => !(left == right); + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(McpSetEnvValueModeDetails left, McpSetEnvValueModeDetails right) => !(left == right); /// - public override bool Equals(object? obj) => obj is McpAppsHostContextDetailsTheme other && Equals(other); + public override bool Equals(object? obj) => obj is McpSetEnvValueModeDetails other && Equals(other); /// - public bool Equals(McpAppsHostContextDetailsTheme other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + public bool Equals(McpSetEnvValueModeDetails other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); /// public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); @@ -35517,62 +36487,62 @@ public McpAppsHostContextDetailsTheme(string value) /// public override string ToString() => Value; - /// Provides a for serializing instances. + /// Provides a for serializing instances. [EditorBrowsable(EditorBrowsableState.Never)] - public sealed class Converter : JsonConverter + public sealed class Converter : JsonConverter { /// - public override McpAppsHostContextDetailsTheme Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override McpSetEnvValueModeDetails Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); } /// - public override void Write(Utf8JsonWriter writer, McpAppsHostContextDetailsTheme value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, McpSetEnvValueModeDetails value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(McpAppsHostContextDetailsTheme)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(McpSetEnvValueModeDetails)); } } } -/// The sender role of an MCP prompt message. +/// Outcome of starting the original prepared owned login. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct McpPromptRole : IEquatable +public readonly struct McpOwnedOauthLoginStatus : IEquatable { private readonly string? _value; - /// Initializes a new instance of the struct. - /// The value to associate with this . + /// Initializes a new instance of the struct. + /// The value to associate with this . [JsonConstructor] - public McpPromptRole(string value) + public McpOwnedOauthLoginStatus(string value) { ArgumentException.ThrowIfNullOrWhiteSpace(value); _value = value; } - /// Gets the value associated with this . + /// Gets the value associated with this . public string Value => _value ?? string.Empty; - /// A message from the user. - public static McpPromptRole User { get; } = new("user"); + /// The original requester may open the returned authorisation URL. + public static McpOwnedOauthLoginStatus AwaitingBrowser { get; } = new("awaiting-browser"); - /// A message from the assistant. - public static McpPromptRole Assistant { get; } = new("assistant"); + /// Cached credentials were accepted and the original server finished reconnecting. + public static McpOwnedOauthLoginStatus Connected { get; } = new("connected"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(McpPromptRole left, McpPromptRole right) => left.Equals(right); + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(McpOwnedOauthLoginStatus left, McpOwnedOauthLoginStatus right) => left.Equals(right); - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(McpPromptRole left, McpPromptRole right) => !(left == right); + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(McpOwnedOauthLoginStatus left, McpOwnedOauthLoginStatus right) => !(left == right); /// - public override bool Equals(object? obj) => obj is McpPromptRole other && Equals(other); + public override bool Equals(object? obj) => obj is McpOwnedOauthLoginStatus other && Equals(other); /// - public bool Equals(McpPromptRole other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + public bool Equals(McpOwnedOauthLoginStatus other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); /// public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); @@ -35580,74 +36550,62 @@ public McpPromptRole(string value) /// public override string ToString() => Value; - /// Provides a for serializing instances. + /// Provides a for serializing instances. [EditorBrowsable(EditorBrowsableState.Never)] - public sealed class Converter : JsonConverter + public sealed class Converter : JsonConverter { /// - public override McpPromptRole Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override McpOwnedOauthLoginStatus Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); } /// - public override void Write(Utf8JsonWriter writer, McpPromptRole value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, McpOwnedOauthLoginStatus value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(McpPromptRole)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(McpOwnedOauthLoginStatus)); } } } -/// Session-scoped diagnostic threshold. Capture is disabled by default and is never persisted with the session. +/// OAuth grant type override for this login. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct DiagnosticLogLevel : IEquatable +public readonly struct McpOauthLoginGrantType : IEquatable { private readonly string? _value; - /// Initializes a new instance of the struct. - /// The value to associate with this . + /// Initializes a new instance of the struct. + /// The value to associate with this . [JsonConstructor] - public DiagnosticLogLevel(string value) + public McpOauthLoginGrantType(string value) { ArgumentException.ThrowIfNullOrWhiteSpace(value); _value = value; } - /// Gets the value associated with this . + /// Gets the value associated with this . public string Value => _value ?? string.Empty; - /// Disable capture and clear retained diagnostics. - public static DiagnosticLogLevel Off { get; } = new("off"); - - /// Capture failures only. - public static DiagnosticLogLevel Error { get; } = new("error"); - - /// Capture failures, warnings, and stderr. - public static DiagnosticLogLevel Warning { get; } = new("warning"); - - /// Capture lifecycle diagnostics. - public static DiagnosticLogLevel Info { get; } = new("info"); - - /// Capture protocol frames and launch diagnostics. - public static DiagnosticLogLevel Debug { get; } = new("debug"); + /// Interactive browser-based OAuth flow using an authorization code, typically with PKCE. + public static McpOauthLoginGrantType AuthorizationCode { get; } = new("authorization_code"); - /// Capture HTTP metadata in addition to debug diagnostics. - public static DiagnosticLogLevel Trace { get; } = new("trace"); + /// Headless OAuth flow where a confidential client authenticates directly with a client secret. + public static McpOauthLoginGrantType ClientCredentials { get; } = new("client_credentials"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(DiagnosticLogLevel left, DiagnosticLogLevel right) => left.Equals(right); + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(McpOauthLoginGrantType left, McpOauthLoginGrantType right) => left.Equals(right); - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(DiagnosticLogLevel left, DiagnosticLogLevel right) => !(left == right); + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(McpOauthLoginGrantType left, McpOauthLoginGrantType right) => !(left == right); /// - public override bool Equals(object? obj) => obj is DiagnosticLogLevel other && Equals(other); + public override bool Equals(object? obj) => obj is McpOauthLoginGrantType other && Equals(other); /// - public bool Equals(DiagnosticLogLevel other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + public bool Equals(McpOauthLoginGrantType other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); /// public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); @@ -35655,62 +36613,65 @@ public DiagnosticLogLevel(string value) /// public override string ToString() => Value; - /// Provides a for serializing instances. + /// Provides a for serializing instances. [EditorBrowsable(EditorBrowsableState.Never)] - public sealed class Converter : JsonConverter + public sealed class Converter : JsonConverter { /// - public override DiagnosticLogLevel Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override McpOauthLoginGrantType Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); } /// - public override void Write(Utf8JsonWriter writer, DiagnosticLogLevel value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, McpOauthLoginGrantType value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(DiagnosticLogLevel)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(McpOauthLoginGrantType)); } } } -/// Whether the supplied diagnostic cursor remained within the retained buffer window. +/// Why a passive MCP OAuth probe determined authentication is needed. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct DiagnosticCursorStatus : IEquatable +public readonly struct McpOauthProbeNeedsAuthReason : IEquatable { private readonly string? _value; - /// Initializes a new instance of the struct. - /// The value to associate with this . + /// Initializes a new instance of the struct. + /// The value to associate with this . [JsonConstructor] - public DiagnosticCursorStatus(string value) + public McpOauthProbeNeedsAuthReason(string value) { ArgumentException.ThrowIfNullOrWhiteSpace(value); _value = value; } - /// Gets the value associated with this . + /// Gets the value associated with this . public string Value => _value ?? string.Empty; - /// The cursor is valid for the current retained window. - public static DiagnosticCursorStatus Ok { get; } = new("ok"); + /// No token was sent and the server requires authentication. + public static McpOauthProbeNeedsAuthReason Initial { get; } = new("initial"); - /// The cursor no longer addresses retained records; reading resumes at the oldest retained record. - public static DiagnosticCursorStatus Expired { get; } = new("expired"); + /// A cached token was sent and rejected. + public static McpOauthProbeNeedsAuthReason Refresh { get; } = new("refresh"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(DiagnosticCursorStatus left, DiagnosticCursorStatus right) => left.Equals(right); + /// The server returned a 403 insufficient_scope challenge, indicating additional scopes or audience are needed. + public static McpOauthProbeNeedsAuthReason Upscope { get; } = new("upscope"); - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(DiagnosticCursorStatus left, DiagnosticCursorStatus right) => !(left == right); + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(McpOauthProbeNeedsAuthReason left, McpOauthProbeNeedsAuthReason right) => left.Equals(right); + + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(McpOauthProbeNeedsAuthReason left, McpOauthProbeNeedsAuthReason right) => !(left == right); /// - public override bool Equals(object? obj) => obj is DiagnosticCursorStatus other && Equals(other); + public override bool Equals(object? obj) => obj is McpOauthProbeNeedsAuthReason other && Equals(other); /// - public bool Equals(DiagnosticCursorStatus other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + public bool Equals(McpOauthProbeNeedsAuthReason other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); /// public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); @@ -35718,62 +36679,65 @@ public DiagnosticCursorStatus(string value) /// public override string ToString() => Value; - /// Provides a for serializing instances. + /// Provides a for serializing instances. [EditorBrowsable(EditorBrowsableState.Never)] - public sealed class Converter : JsonConverter + public sealed class Converter : JsonConverter { /// - public override DiagnosticCursorStatus Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override McpOauthProbeNeedsAuthReason Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); } /// - public override void Write(Utf8JsonWriter writer, DiagnosticCursorStatus value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, McpOauthProbeNeedsAuthReason value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(DiagnosticCursorStatus)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(McpOauthProbeNeedsAuthReason)); } } } -/// Direction of an observed MCP protocol frame. +/// Allowed values for the `McpAppsSetHostContextDetailsAvailableDisplayMode` enumeration. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct McpDiagnosticDirection : IEquatable +public readonly struct McpAppsSetHostContextDetailsAvailableDisplayMode : IEquatable { private readonly string? _value; - /// Initializes a new instance of the struct. - /// The value to associate with this . + /// Initializes a new instance of the struct. + /// The value to associate with this . [JsonConstructor] - public McpDiagnosticDirection(string value) + public McpAppsSetHostContextDetailsAvailableDisplayMode(string value) { ArgumentException.ThrowIfNullOrWhiteSpace(value); _value = value; } - /// Gets the value associated with this . + /// Gets the value associated with this . public string Value => _value ?? string.Empty; - /// Frame emitted by the Copilot MCP client. - public static McpDiagnosticDirection ClientToServer { get; } = new("client-to-server"); + /// Rendered inline within the host conversation surface. + public static McpAppsSetHostContextDetailsAvailableDisplayMode Inline { get; } = new("inline"); - /// Frame received from the MCP server. - public static McpDiagnosticDirection ServerToClient { get; } = new("server-to-client"); + /// Rendered as a fullscreen overlay. + public static McpAppsSetHostContextDetailsAvailableDisplayMode Fullscreen { get; } = new("fullscreen"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(McpDiagnosticDirection left, McpDiagnosticDirection right) => left.Equals(right); + /// Rendered as a picture-in-picture floating panel. + public static McpAppsSetHostContextDetailsAvailableDisplayMode Pip { get; } = new("pip"); - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(McpDiagnosticDirection left, McpDiagnosticDirection right) => !(left == right); + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(McpAppsSetHostContextDetailsAvailableDisplayMode left, McpAppsSetHostContextDetailsAvailableDisplayMode right) => left.Equals(right); + + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(McpAppsSetHostContextDetailsAvailableDisplayMode left, McpAppsSetHostContextDetailsAvailableDisplayMode right) => !(left == right); /// - public override bool Equals(object? obj) => obj is McpDiagnosticDirection other && Equals(other); + public override bool Equals(object? obj) => obj is McpAppsSetHostContextDetailsAvailableDisplayMode other && Equals(other); /// - public bool Equals(McpDiagnosticDirection other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + public bool Equals(McpAppsSetHostContextDetailsAvailableDisplayMode other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); /// public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); @@ -35781,68 +36745,65 @@ public McpDiagnosticDirection(string value) /// public override string ToString() => Value; - /// Provides a for serializing instances. + /// Provides a for serializing instances. [EditorBrowsable(EditorBrowsableState.Never)] - public sealed class Converter : JsonConverter + public sealed class Converter : JsonConverter { /// - public override McpDiagnosticDirection Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override McpAppsSetHostContextDetailsAvailableDisplayMode Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); } /// - public override void Write(Utf8JsonWriter writer, McpDiagnosticDirection value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, McpAppsSetHostContextDetailsAvailableDisplayMode value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(McpDiagnosticDirection)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(McpAppsSetHostContextDetailsAvailableDisplayMode)); } } } -/// Category for an MCP diagnostic record. +/// Current display mode (SEP-1865). [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct McpDiagnosticKind : IEquatable +public readonly struct McpAppsSetHostContextDetailsDisplayMode : IEquatable { private readonly string? _value; - /// Initializes a new instance of the struct. - /// The value to associate with this . + /// Initializes a new instance of the struct. + /// The value to associate with this . [JsonConstructor] - public McpDiagnosticKind(string value) + public McpAppsSetHostContextDetailsDisplayMode(string value) { ArgumentException.ThrowIfNullOrWhiteSpace(value); _value = value; } - /// Gets the value associated with this . + /// Gets the value associated with this . public string Value => _value ?? string.Empty; - /// Connection lifecycle transition or failure. - public static McpDiagnosticKind Lifecycle { get; } = new("lifecycle"); - - /// JSON-RPC protocol frame. - public static McpDiagnosticKind Protocol { get; } = new("protocol"); + /// Rendered inline within the host conversation surface. + public static McpAppsSetHostContextDetailsDisplayMode Inline { get; } = new("inline"); - /// HTTP request or response metadata. - public static McpDiagnosticKind Http { get; } = new("http"); + /// Rendered as a fullscreen overlay. + public static McpAppsSetHostContextDetailsDisplayMode Fullscreen { get; } = new("fullscreen"); - /// Local MCP server standard-error output. - public static McpDiagnosticKind Stderr { get; } = new("stderr"); + /// Rendered as a picture-in-picture floating panel. + public static McpAppsSetHostContextDetailsDisplayMode Pip { get; } = new("pip"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(McpDiagnosticKind left, McpDiagnosticKind right) => left.Equals(right); + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(McpAppsSetHostContextDetailsDisplayMode left, McpAppsSetHostContextDetailsDisplayMode right) => left.Equals(right); - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(McpDiagnosticKind left, McpDiagnosticKind right) => !(left == right); + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(McpAppsSetHostContextDetailsDisplayMode left, McpAppsSetHostContextDetailsDisplayMode right) => !(left == right); /// - public override bool Equals(object? obj) => obj is McpDiagnosticKind other && Equals(other); + public override bool Equals(object? obj) => obj is McpAppsSetHostContextDetailsDisplayMode other && Equals(other); /// - public bool Equals(McpDiagnosticKind other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + public bool Equals(McpAppsSetHostContextDetailsDisplayMode other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); /// public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); @@ -35850,71 +36811,65 @@ public McpDiagnosticKind(string value) /// public override string ToString() => Value; - /// Provides a for serializing instances. + /// Provides a for serializing instances. [EditorBrowsable(EditorBrowsableState.Never)] - public sealed class Converter : JsonConverter + public sealed class Converter : JsonConverter { /// - public override McpDiagnosticKind Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override McpAppsSetHostContextDetailsDisplayMode Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); } /// - public override void Write(Utf8JsonWriter writer, McpDiagnosticKind value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, McpAppsSetHostContextDetailsDisplayMode value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(McpDiagnosticKind)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(McpAppsSetHostContextDetailsDisplayMode)); } } } -/// Severity of an emitted diagnostic record. +/// Platform type for responsive design. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct DiagnosticSeverity : IEquatable +public readonly struct McpAppsSetHostContextDetailsPlatform : IEquatable { private readonly string? _value; - /// Initializes a new instance of the struct. - /// The value to associate with this . + /// Initializes a new instance of the struct. + /// The value to associate with this . [JsonConstructor] - public DiagnosticSeverity(string value) + public McpAppsSetHostContextDetailsPlatform(string value) { ArgumentException.ThrowIfNullOrWhiteSpace(value); _value = value; } - /// Gets the value associated with this . + /// Gets the value associated with this . public string Value => _value ?? string.Empty; - /// Failure that prevented or interrupted communication. - public static DiagnosticSeverity Error { get; } = new("error"); - - /// A recoverable warning or server standard-error output. - public static DiagnosticSeverity Warning { get; } = new("warning"); - - /// Lifecycle transition. - public static DiagnosticSeverity Info { get; } = new("info"); + /// Host runs in a web browser. + public static McpAppsSetHostContextDetailsPlatform Web { get; } = new("web"); - /// Protocol-frame or launch diagnostic. - public static DiagnosticSeverity Debug { get; } = new("debug"); + /// Host runs as a desktop application. + public static McpAppsSetHostContextDetailsPlatform Desktop { get; } = new("desktop"); - /// HTTP metadata diagnostic. - public static DiagnosticSeverity Trace { get; } = new("trace"); + /// Host runs on a mobile device. + public static McpAppsSetHostContextDetailsPlatform Mobile { get; } = new("mobile"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(DiagnosticSeverity left, DiagnosticSeverity right) => left.Equals(right); + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(McpAppsSetHostContextDetailsPlatform left, McpAppsSetHostContextDetailsPlatform right) => left.Equals(right); - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(DiagnosticSeverity left, DiagnosticSeverity right) => !(left == right); + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(McpAppsSetHostContextDetailsPlatform left, McpAppsSetHostContextDetailsPlatform right) => !(left == right); /// - public override bool Equals(object? obj) => obj is DiagnosticSeverity other && Equals(other); + public override bool Equals(object? obj) => obj is McpAppsSetHostContextDetailsPlatform other && Equals(other); /// - public bool Equals(DiagnosticSeverity other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + public bool Equals(McpAppsSetHostContextDetailsPlatform other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); /// public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); @@ -35922,59 +36877,62 @@ public DiagnosticSeverity(string value) /// public override string ToString() => Value; - /// Provides a for serializing instances. + /// Provides a for serializing instances. [EditorBrowsable(EditorBrowsableState.Never)] - public sealed class Converter : JsonConverter + public sealed class Converter : JsonConverter { /// - public override DiagnosticSeverity Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override McpAppsSetHostContextDetailsPlatform Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); } /// - public override void Write(Utf8JsonWriter writer, DiagnosticSeverity value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, McpAppsSetHostContextDetailsPlatform value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(DiagnosticSeverity)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(McpAppsSetHostContextDetailsPlatform)); } } } -/// A supported diagnostic source. +/// UI theme preference per SEP-1865. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct DiagnosticSource : IEquatable +public readonly struct McpAppsSetHostContextDetailsTheme : IEquatable { private readonly string? _value; - /// Initializes a new instance of the struct. - /// The value to associate with this . + /// Initializes a new instance of the struct. + /// The value to associate with this . [JsonConstructor] - public DiagnosticSource(string value) + public McpAppsSetHostContextDetailsTheme(string value) { ArgumentException.ThrowIfNullOrWhiteSpace(value); _value = value; } - /// Gets the value associated with this . + /// Gets the value associated with this . public string Value => _value ?? string.Empty; - /// Gets the mcp value. - public static DiagnosticSource Mcp { get; } = new("mcp"); + /// Light UI theme. + public static McpAppsSetHostContextDetailsTheme Light { get; } = new("light"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(DiagnosticSource left, DiagnosticSource right) => left.Equals(right); + /// Dark UI theme. + public static McpAppsSetHostContextDetailsTheme Dark { get; } = new("dark"); - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(DiagnosticSource left, DiagnosticSource right) => !(left == right); + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(McpAppsSetHostContextDetailsTheme left, McpAppsSetHostContextDetailsTheme right) => left.Equals(right); + + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(McpAppsSetHostContextDetailsTheme left, McpAppsSetHostContextDetailsTheme right) => !(left == right); /// - public override bool Equals(object? obj) => obj is DiagnosticSource other && Equals(other); + public override bool Equals(object? obj) => obj is McpAppsSetHostContextDetailsTheme other && Equals(other); /// - public bool Equals(DiagnosticSource other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + public bool Equals(McpAppsSetHostContextDetailsTheme other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); /// public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); @@ -35982,65 +36940,65 @@ public DiagnosticSource(string value) /// public override string ToString() => Value; - /// Provides a for serializing instances. + /// Provides a for serializing instances. [EditorBrowsable(EditorBrowsableState.Never)] - public sealed class Converter : JsonConverter + public sealed class Converter : JsonConverter { /// - public override DiagnosticSource Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override McpAppsSetHostContextDetailsTheme Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); } /// - public override void Write(Utf8JsonWriter writer, DiagnosticSource value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, McpAppsSetHostContextDetailsTheme value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(DiagnosticSource)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(McpAppsSetHostContextDetailsTheme)); } } } -/// Availability of the EXPERIMENTAL session connector API. +/// Allowed values for the `McpAppsHostContextDetailsAvailableDisplayMode` enumeration. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct ConnectorAvailability : IEquatable +public readonly struct McpAppsHostContextDetailsAvailableDisplayMode : IEquatable { private readonly string? _value; - /// Initializes a new instance of the struct. - /// The value to associate with this . + /// Initializes a new instance of the struct. + /// The value to associate with this . [JsonConstructor] - public ConnectorAvailability(string value) + public McpAppsHostContextDetailsAvailableDisplayMode(string value) { ArgumentException.ThrowIfNullOrWhiteSpace(value); _value = value; } - /// Gets the value associated with this . + /// Gets the value associated with this . public string Value => _value ?? string.Empty; - /// The resolved Connector feature is enabled and Connector requests are permitted. - public static ConnectorAvailability Enabled { get; } = new("enabled"); + /// Rendered inline within the host conversation surface. + public static McpAppsHostContextDetailsAvailableDisplayMode Inline { get; } = new("inline"); - /// The resolved Connector feature is off. No Connector service request is made while disabled. - public static ConnectorAvailability Disabled { get; } = new("disabled"); + /// Rendered as a fullscreen overlay. + public static McpAppsHostContextDetailsAvailableDisplayMode Fullscreen { get; } = new("fullscreen"); - /// The session has no eligible host-owned GitHub account or does not support local Connector projection. - public static ConnectorAvailability Unavailable { get; } = new("unavailable"); + /// Rendered as a picture-in-picture floating panel. + public static McpAppsHostContextDetailsAvailableDisplayMode Pip { get; } = new("pip"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(ConnectorAvailability left, ConnectorAvailability right) => left.Equals(right); + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(McpAppsHostContextDetailsAvailableDisplayMode left, McpAppsHostContextDetailsAvailableDisplayMode right) => left.Equals(right); - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(ConnectorAvailability left, ConnectorAvailability right) => !(left == right); + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(McpAppsHostContextDetailsAvailableDisplayMode left, McpAppsHostContextDetailsAvailableDisplayMode right) => !(left == right); /// - public override bool Equals(object? obj) => obj is ConnectorAvailability other && Equals(other); + public override bool Equals(object? obj) => obj is McpAppsHostContextDetailsAvailableDisplayMode other && Equals(other); /// - public bool Equals(ConnectorAvailability other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + public bool Equals(McpAppsHostContextDetailsAvailableDisplayMode other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); /// public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); @@ -36048,59 +37006,65 @@ public ConnectorAvailability(string value) /// public override string ToString() => Value; - /// Provides a for serializing instances. + /// Provides a for serializing instances. [EditorBrowsable(EditorBrowsableState.Never)] - public sealed class Converter : JsonConverter + public sealed class Converter : JsonConverter { /// - public override ConnectorAvailability Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override McpAppsHostContextDetailsAvailableDisplayMode Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); } /// - public override void Write(Utf8JsonWriter writer, ConnectorAvailability value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, McpAppsHostContextDetailsAvailableDisplayMode value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(ConnectorAvailability)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(McpAppsHostContextDetailsAvailableDisplayMode)); } } } -/// Stable OAuth scope whose absence prevents Connector management. +/// Current display mode (SEP-1865). [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct ConnectorAuthorizationScope : IEquatable +public readonly struct McpAppsHostContextDetailsDisplayMode : IEquatable { private readonly string? _value; - /// Initializes a new instance of the struct. - /// The value to associate with this . + /// Initializes a new instance of the struct. + /// The value to associate with this . [JsonConstructor] - public ConnectorAuthorizationScope(string value) + public McpAppsHostContextDetailsDisplayMode(string value) { ArgumentException.ThrowIfNullOrWhiteSpace(value); _value = value; } - /// Gets the value associated with this . + /// Gets the value associated with this . public string Value => _value ?? string.Empty; - /// Allows Copilot to manage Connector connections and use their tools. - public static ConnectorAuthorizationScope WritePluginGatewayConnections { get; } = new("write_plugin_gateway_connections"); + /// Rendered inline within the host conversation surface. + public static McpAppsHostContextDetailsDisplayMode Inline { get; } = new("inline"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(ConnectorAuthorizationScope left, ConnectorAuthorizationScope right) => left.Equals(right); + /// Rendered as a fullscreen overlay. + public static McpAppsHostContextDetailsDisplayMode Fullscreen { get; } = new("fullscreen"); - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(ConnectorAuthorizationScope left, ConnectorAuthorizationScope right) => !(left == right); + /// Rendered as a picture-in-picture floating panel. + public static McpAppsHostContextDetailsDisplayMode Pip { get; } = new("pip"); + + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(McpAppsHostContextDetailsDisplayMode left, McpAppsHostContextDetailsDisplayMode right) => left.Equals(right); + + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(McpAppsHostContextDetailsDisplayMode left, McpAppsHostContextDetailsDisplayMode right) => !(left == right); /// - public override bool Equals(object? obj) => obj is ConnectorAuthorizationScope other && Equals(other); + public override bool Equals(object? obj) => obj is McpAppsHostContextDetailsDisplayMode other && Equals(other); /// - public bool Equals(ConnectorAuthorizationScope other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + public bool Equals(McpAppsHostContextDetailsDisplayMode other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); /// public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); @@ -36108,71 +37072,65 @@ public ConnectorAuthorizationScope(string value) /// public override string ToString() => Value; - /// Provides a for serializing instances. + /// Provides a for serializing instances. [EditorBrowsable(EditorBrowsableState.Never)] - public sealed class Converter : JsonConverter + public sealed class Converter : JsonConverter { /// - public override ConnectorAuthorizationScope Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override McpAppsHostContextDetailsDisplayMode Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); } /// - public override void Write(Utf8JsonWriter writer, ConnectorAuthorizationScope value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, McpAppsHostContextDetailsDisplayMode value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(ConnectorAuthorizationScope)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(McpAppsHostContextDetailsDisplayMode)); } } } -/// Authoritative service connection state for one Connector. +/// Platform type for responsive design. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct ConnectorCatalogStatus : IEquatable +public readonly struct McpAppsHostContextDetailsPlatform : IEquatable { private readonly string? _value; - /// Initializes a new instance of the struct. - /// The value to associate with this . + /// Initializes a new instance of the struct. + /// The value to associate with this . [JsonConstructor] - public ConnectorCatalogStatus(string value) + public McpAppsHostContextDetailsPlatform(string value) { ArgumentException.ThrowIfNullOrWhiteSpace(value); _value = value; } - /// Gets the value associated with this . + /// Gets the value associated with this . public string Value => _value ?? string.Empty; - /// The Connector is available but not connected. - public static ConnectorCatalogStatus NotConnected { get; } = new("not_connected"); - - /// The Connector service is still completing connection or consent. - public static ConnectorCatalogStatus Pending { get; } = new("pending"); - - /// The Connector is connected and may contribute MCP servers. - public static ConnectorCatalogStatus Connected { get; } = new("connected"); + /// Host runs in a web browser. + public static McpAppsHostContextDetailsPlatform Web { get; } = new("web"); - /// The Connector service reports an unusable connection. - public static ConnectorCatalogStatus Error { get; } = new("error"); + /// Host runs as a desktop application. + public static McpAppsHostContextDetailsPlatform Desktop { get; } = new("desktop"); - /// The service returned a future or unrecognized state. - public static ConnectorCatalogStatus Unknown { get; } = new("unknown"); + /// Host runs on a mobile device. + public static McpAppsHostContextDetailsPlatform Mobile { get; } = new("mobile"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(ConnectorCatalogStatus left, ConnectorCatalogStatus right) => left.Equals(right); + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(McpAppsHostContextDetailsPlatform left, McpAppsHostContextDetailsPlatform right) => left.Equals(right); - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(ConnectorCatalogStatus left, ConnectorCatalogStatus right) => !(left == right); + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(McpAppsHostContextDetailsPlatform left, McpAppsHostContextDetailsPlatform right) => !(left == right); /// - public override bool Equals(object? obj) => obj is ConnectorCatalogStatus other && Equals(other); + public override bool Equals(object? obj) => obj is McpAppsHostContextDetailsPlatform other && Equals(other); /// - public bool Equals(ConnectorCatalogStatus other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + public bool Equals(McpAppsHostContextDetailsPlatform other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); /// public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); @@ -36180,77 +37138,62 @@ public ConnectorCatalogStatus(string value) /// public override string ToString() => Value; - /// Provides a for serializing instances. + /// Provides a for serializing instances. [EditorBrowsable(EditorBrowsableState.Never)] - public sealed class Converter : JsonConverter + public sealed class Converter : JsonConverter { /// - public override ConnectorCatalogStatus Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override McpAppsHostContextDetailsPlatform Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); } /// - public override void Write(Utf8JsonWriter writer, ConnectorCatalogStatus value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, McpAppsHostContextDetailsPlatform value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(ConnectorCatalogStatus)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(McpAppsHostContextDetailsPlatform)); } } } -/// Live MCP status of one Connector-owned runtime server. +/// UI theme preference per SEP-1865. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct ConnectorMcpStatus : IEquatable +public readonly struct McpAppsHostContextDetailsTheme : IEquatable { private readonly string? _value; - /// Initializes a new instance of the struct. - /// The value to associate with this . + /// Initializes a new instance of the struct. + /// The value to associate with this . [JsonConstructor] - public ConnectorMcpStatus(string value) + public McpAppsHostContextDetailsTheme(string value) { ArgumentException.ThrowIfNullOrWhiteSpace(value); _value = value; } - /// Gets the value associated with this . + /// Gets the value associated with this . public string Value => _value ?? string.Empty; - /// The server is connected and its tools are available. - public static ConnectorMcpStatus Connected { get; } = new("connected"); - - /// The server connection is still being established. - public static ConnectorMcpStatus Pending { get; } = new("pending"); - - /// The server requires refreshed GitHub authorization. - public static ConnectorMcpStatus NeedsAuth { get; } = new("needs_auth"); - - /// The server failed to connect or initialize. - public static ConnectorMcpStatus Failed { get; } = new("failed"); - - /// The server is intentionally stopped, including when managed policy blocks it. - public static ConnectorMcpStatus Stopped { get; } = new("stopped"); - - /// The server is configured but explicitly disabled. - public static ConnectorMcpStatus Disabled { get; } = new("disabled"); + /// Light UI theme. + public static McpAppsHostContextDetailsTheme Light { get; } = new("light"); - /// The Connector currently has no live server configuration. - public static ConnectorMcpStatus NotConfigured { get; } = new("not_configured"); + /// Dark UI theme. + public static McpAppsHostContextDetailsTheme Dark { get; } = new("dark"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(ConnectorMcpStatus left, ConnectorMcpStatus right) => left.Equals(right); + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(McpAppsHostContextDetailsTheme left, McpAppsHostContextDetailsTheme right) => left.Equals(right); - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(ConnectorMcpStatus left, ConnectorMcpStatus right) => !(left == right); + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(McpAppsHostContextDetailsTheme left, McpAppsHostContextDetailsTheme right) => !(left == right); /// - public override bool Equals(object? obj) => obj is ConnectorMcpStatus other && Equals(other); + public override bool Equals(object? obj) => obj is McpAppsHostContextDetailsTheme other && Equals(other); /// - public bool Equals(ConnectorMcpStatus other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + public bool Equals(McpAppsHostContextDetailsTheme other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); /// public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); @@ -36258,62 +37201,62 @@ public ConnectorMcpStatus(string value) /// public override string ToString() => Value; - /// Provides a for serializing instances. + /// Provides a for serializing instances. [EditorBrowsable(EditorBrowsableState.Never)] - public sealed class Converter : JsonConverter + public sealed class Converter : JsonConverter { /// - public override ConnectorMcpStatus Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override McpAppsHostContextDetailsTheme Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); } /// - public override void Write(Utf8JsonWriter writer, ConnectorMcpStatus value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, McpAppsHostContextDetailsTheme value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(ConnectorMcpStatus)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(McpAppsHostContextDetailsTheme)); } } } -/// Transport to be used for provider requests. +/// The sender role of an MCP prompt message. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct ProviderEndpointTransport : IEquatable +public readonly struct McpPromptRole : IEquatable { private readonly string? _value; - /// Initializes a new instance of the struct. - /// The value to associate with this . + /// Initializes a new instance of the struct. + /// The value to associate with this . [JsonConstructor] - public ProviderEndpointTransport(string value) + public McpPromptRole(string value) { ArgumentException.ThrowIfNullOrWhiteSpace(value); _value = value; } - /// Gets the value associated with this . + /// Gets the value associated with this . public string Value => _value ?? string.Empty; - /// HTTP request/streaming transport. - public static ProviderEndpointTransport Http { get; } = new("http"); + /// A message from the user. + public static McpPromptRole User { get; } = new("user"); - /// WebSocket transport. - public static ProviderEndpointTransport Websockets { get; } = new("websockets"); + /// A message from the assistant. + public static McpPromptRole Assistant { get; } = new("assistant"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(ProviderEndpointTransport left, ProviderEndpointTransport right) => left.Equals(right); + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(McpPromptRole left, McpPromptRole right) => left.Equals(right); - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(ProviderEndpointTransport left, ProviderEndpointTransport right) => !(left == right); + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(McpPromptRole left, McpPromptRole right) => !(left == right); /// - public override bool Equals(object? obj) => obj is ProviderEndpointTransport other && Equals(other); + public override bool Equals(object? obj) => obj is McpPromptRole other && Equals(other); /// - public bool Equals(ProviderEndpointTransport other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + public bool Equals(McpPromptRole other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); /// public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); @@ -36321,65 +37264,74 @@ public ProviderEndpointTransport(string value) /// public override string ToString() => Value; - /// Provides a for serializing instances. + /// Provides a for serializing instances. [EditorBrowsable(EditorBrowsableState.Never)] - public sealed class Converter : JsonConverter + public sealed class Converter : JsonConverter { /// - public override ProviderEndpointTransport Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override McpPromptRole Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); } /// - public override void Write(Utf8JsonWriter writer, ProviderEndpointTransport value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, McpPromptRole value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(ProviderEndpointTransport)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(McpPromptRole)); } } } -/// Provider family. Matches the `type` field of a BYOK provider config. +/// Session-scoped diagnostic threshold. Capture is disabled by default and is never persisted with the session. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct ProviderEndpointType : IEquatable +public readonly struct DiagnosticLogLevel : IEquatable { private readonly string? _value; - /// Initializes a new instance of the struct. - /// The value to associate with this . + /// Initializes a new instance of the struct. + /// The value to associate with this . [JsonConstructor] - public ProviderEndpointType(string value) + public DiagnosticLogLevel(string value) { ArgumentException.ThrowIfNullOrWhiteSpace(value); _value = value; } - /// Gets the value associated with this . + /// Gets the value associated with this . public string Value => _value ?? string.Empty; - /// OpenAI-compatible endpoint (use the OpenAI client library). - public static ProviderEndpointType Openai { get; } = new("openai"); + /// Disable capture and clear retained diagnostics. + public static DiagnosticLogLevel Off { get; } = new("off"); - /// Azure OpenAI endpoint (use the OpenAI client library with the Azure base URL). - public static ProviderEndpointType Azure { get; } = new("azure"); + /// Capture failures only. + public static DiagnosticLogLevel Error { get; } = new("error"); - /// Anthropic endpoint (use the Anthropic client library). - public static ProviderEndpointType Anthropic { get; } = new("anthropic"); + /// Capture failures, warnings, and stderr. + public static DiagnosticLogLevel Warning { get; } = new("warning"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(ProviderEndpointType left, ProviderEndpointType right) => left.Equals(right); + /// Capture lifecycle diagnostics. + public static DiagnosticLogLevel Info { get; } = new("info"); - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(ProviderEndpointType left, ProviderEndpointType right) => !(left == right); + /// Capture protocol frames and launch diagnostics. + public static DiagnosticLogLevel Debug { get; } = new("debug"); + + /// Capture HTTP metadata in addition to debug diagnostics. + public static DiagnosticLogLevel Trace { get; } = new("trace"); + + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(DiagnosticLogLevel left, DiagnosticLogLevel right) => left.Equals(right); + + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(DiagnosticLogLevel left, DiagnosticLogLevel right) => !(left == right); /// - public override bool Equals(object? obj) => obj is ProviderEndpointType other && Equals(other); + public override bool Equals(object? obj) => obj is DiagnosticLogLevel other && Equals(other); /// - public bool Equals(ProviderEndpointType other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + public bool Equals(DiagnosticLogLevel other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); /// public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); @@ -36387,62 +37339,62 @@ public ProviderEndpointType(string value) /// public override string ToString() => Value; - /// Provides a for serializing instances. + /// Provides a for serializing instances. [EditorBrowsable(EditorBrowsableState.Never)] - public sealed class Converter : JsonConverter + public sealed class Converter : JsonConverter { /// - public override ProviderEndpointType Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override DiagnosticLogLevel Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); } /// - public override void Write(Utf8JsonWriter writer, ProviderEndpointType value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, DiagnosticLogLevel value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(ProviderEndpointType)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(DiagnosticLogLevel)); } } } -/// Wire API to be used, when required for the provider type. +/// Whether the supplied diagnostic cursor remained within the retained buffer window. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct ProviderEndpointWireApi : IEquatable +public readonly struct DiagnosticCursorStatus : IEquatable { private readonly string? _value; - /// Initializes a new instance of the struct. - /// The value to associate with this . + /// Initializes a new instance of the struct. + /// The value to associate with this . [JsonConstructor] - public ProviderEndpointWireApi(string value) + public DiagnosticCursorStatus(string value) { ArgumentException.ThrowIfNullOrWhiteSpace(value); _value = value; } - /// Gets the value associated with this . + /// Gets the value associated with this . public string Value => _value ?? string.Empty; - /// Classic chat-completions request shape. - public static ProviderEndpointWireApi Completions { get; } = new("completions"); + /// The cursor is valid for the current retained window. + public static DiagnosticCursorStatus Ok { get; } = new("ok"); - /// Newer responses request shape. - public static ProviderEndpointWireApi Responses { get; } = new("responses"); + /// The cursor no longer addresses retained records; reading resumes at the oldest retained record. + public static DiagnosticCursorStatus Expired { get; } = new("expired"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(ProviderEndpointWireApi left, ProviderEndpointWireApi right) => left.Equals(right); + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(DiagnosticCursorStatus left, DiagnosticCursorStatus right) => left.Equals(right); - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(ProviderEndpointWireApi left, ProviderEndpointWireApi right) => !(left == right); + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(DiagnosticCursorStatus left, DiagnosticCursorStatus right) => !(left == right); /// - public override bool Equals(object? obj) => obj is ProviderEndpointWireApi other && Equals(other); + public override bool Equals(object? obj) => obj is DiagnosticCursorStatus other && Equals(other); /// - public bool Equals(ProviderEndpointWireApi other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + public bool Equals(DiagnosticCursorStatus other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); /// public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); @@ -36450,59 +37402,62 @@ public ProviderEndpointWireApi(string value) /// public override string ToString() => Value; - /// Provides a for serializing instances. + /// Provides a for serializing instances. [EditorBrowsable(EditorBrowsableState.Never)] - public sealed class Converter : JsonConverter + public sealed class Converter : JsonConverter { /// - public override ProviderEndpointWireApi Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override DiagnosticCursorStatus Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); } /// - public override void Write(Utf8JsonWriter writer, ProviderEndpointWireApi value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, DiagnosticCursorStatus value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(ProviderEndpointWireApi)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(DiagnosticCursorStatus)); } } } -/// Defines the allowed values. +/// Direction of an observed MCP protocol frame. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct ProtocolAppendMode : IEquatable +public readonly struct McpDiagnosticDirection : IEquatable { private readonly string? _value; - /// Initializes a new instance of the struct. - /// The value to associate with this . + /// Initializes a new instance of the struct. + /// The value to associate with this . [JsonConstructor] - public ProtocolAppendMode(string value) + public McpDiagnosticDirection(string value) { ArgumentException.ThrowIfNullOrWhiteSpace(value); _value = value; } - /// Gets the value associated with this . + /// Gets the value associated with this . public string Value => _value ?? string.Empty; - /// Gets the append value. - public static ProtocolAppendMode Append { get; } = new("append"); + /// Frame emitted by the Copilot MCP client. + public static McpDiagnosticDirection ClientToServer { get; } = new("client-to-server"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(ProtocolAppendMode left, ProtocolAppendMode right) => left.Equals(right); + /// Frame received from the MCP server. + public static McpDiagnosticDirection ServerToClient { get; } = new("server-to-client"); - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(ProtocolAppendMode left, ProtocolAppendMode right) => !(left == right); + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(McpDiagnosticDirection left, McpDiagnosticDirection right) => left.Equals(right); + + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(McpDiagnosticDirection left, McpDiagnosticDirection right) => !(left == right); /// - public override bool Equals(object? obj) => obj is ProtocolAppendMode other && Equals(other); + public override bool Equals(object? obj) => obj is McpDiagnosticDirection other && Equals(other); /// - public bool Equals(ProtocolAppendMode other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + public bool Equals(McpDiagnosticDirection other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); /// public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); @@ -36510,59 +37465,68 @@ public ProtocolAppendMode(string value) /// public override string ToString() => Value; - /// Provides a for serializing instances. + /// Provides a for serializing instances. [EditorBrowsable(EditorBrowsableState.Never)] - public sealed class Converter : JsonConverter + public sealed class Converter : JsonConverter { /// - public override ProtocolAppendMode Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override McpDiagnosticDirection Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); } /// - public override void Write(Utf8JsonWriter writer, ProtocolAppendMode value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, McpDiagnosticDirection value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(ProtocolAppendMode)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(McpDiagnosticDirection)); } } } -/// Defines the allowed values. +/// Category for an MCP diagnostic record. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct ProtocolReplaceMode : IEquatable +public readonly struct McpDiagnosticKind : IEquatable { private readonly string? _value; - /// Initializes a new instance of the struct. - /// The value to associate with this . + /// Initializes a new instance of the struct. + /// The value to associate with this . [JsonConstructor] - public ProtocolReplaceMode(string value) + public McpDiagnosticKind(string value) { ArgumentException.ThrowIfNullOrWhiteSpace(value); _value = value; } - /// Gets the value associated with this . + /// Gets the value associated with this . public string Value => _value ?? string.Empty; - /// Gets the replace value. - public static ProtocolReplaceMode Replace { get; } = new("replace"); + /// Connection lifecycle transition or failure. + public static McpDiagnosticKind Lifecycle { get; } = new("lifecycle"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(ProtocolReplaceMode left, ProtocolReplaceMode right) => left.Equals(right); + /// JSON-RPC protocol frame. + public static McpDiagnosticKind Protocol { get; } = new("protocol"); - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(ProtocolReplaceMode left, ProtocolReplaceMode right) => !(left == right); + /// HTTP request or response metadata. + public static McpDiagnosticKind Http { get; } = new("http"); + + /// Local MCP server standard-error output. + public static McpDiagnosticKind Stderr { get; } = new("stderr"); + + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(McpDiagnosticKind left, McpDiagnosticKind right) => left.Equals(right); + + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(McpDiagnosticKind left, McpDiagnosticKind right) => !(left == right); /// - public override bool Equals(object? obj) => obj is ProtocolReplaceMode other && Equals(other); + public override bool Equals(object? obj) => obj is McpDiagnosticKind other && Equals(other); /// - public bool Equals(ProtocolReplaceMode other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + public bool Equals(McpDiagnosticKind other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); /// public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); @@ -36570,59 +37534,71 @@ public ProtocolReplaceMode(string value) /// public override string ToString() => Value; - /// Provides a for serializing instances. + /// Provides a for serializing instances. [EditorBrowsable(EditorBrowsableState.Never)] - public sealed class Converter : JsonConverter + public sealed class Converter : JsonConverter { /// - public override ProtocolReplaceMode Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override McpDiagnosticKind Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); } /// - public override void Write(Utf8JsonWriter writer, ProtocolReplaceMode value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, McpDiagnosticKind value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(ProtocolReplaceMode)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(McpDiagnosticKind)); } } } -/// Defines the allowed values. +/// Severity of an emitted diagnostic record. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct ProtocolCustomizeMode : IEquatable +public readonly struct DiagnosticSeverity : IEquatable { private readonly string? _value; - /// Initializes a new instance of the struct. - /// The value to associate with this . + /// Initializes a new instance of the struct. + /// The value to associate with this . [JsonConstructor] - public ProtocolCustomizeMode(string value) + public DiagnosticSeverity(string value) { ArgumentException.ThrowIfNullOrWhiteSpace(value); _value = value; } - /// Gets the value associated with this . + /// Gets the value associated with this . public string Value => _value ?? string.Empty; - /// Gets the customize value. - public static ProtocolCustomizeMode Customize { get; } = new("customize"); + /// Failure that prevented or interrupted communication. + public static DiagnosticSeverity Error { get; } = new("error"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(ProtocolCustomizeMode left, ProtocolCustomizeMode right) => left.Equals(right); + /// A recoverable warning or server standard-error output. + public static DiagnosticSeverity Warning { get; } = new("warning"); - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(ProtocolCustomizeMode left, ProtocolCustomizeMode right) => !(left == right); + /// Lifecycle transition. + public static DiagnosticSeverity Info { get; } = new("info"); + + /// Protocol-frame or launch diagnostic. + public static DiagnosticSeverity Debug { get; } = new("debug"); + + /// HTTP metadata diagnostic. + public static DiagnosticSeverity Trace { get; } = new("trace"); + + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(DiagnosticSeverity left, DiagnosticSeverity right) => left.Equals(right); + + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(DiagnosticSeverity left, DiagnosticSeverity right) => !(left == right); /// - public override bool Equals(object? obj) => obj is ProtocolCustomizeMode other && Equals(other); + public override bool Equals(object? obj) => obj is DiagnosticSeverity other && Equals(other); /// - public bool Equals(ProtocolCustomizeMode other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + public bool Equals(DiagnosticSeverity other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); /// public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); @@ -36630,68 +37606,59 @@ public ProtocolCustomizeMode(string value) /// public override string ToString() => Value; - /// Provides a for serializing instances. + /// Provides a for serializing instances. [EditorBrowsable(EditorBrowsableState.Never)] - public sealed class Converter : JsonConverter + public sealed class Converter : JsonConverter { /// - public override ProtocolCustomizeMode Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override DiagnosticSeverity Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); } /// - public override void Write(Utf8JsonWriter writer, ProtocolCustomizeMode value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, DiagnosticSeverity value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(ProtocolCustomizeMode)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(DiagnosticSeverity)); } } } -/// Defines the allowed values. +/// A supported diagnostic source. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct ProtocolStaticSectionAction : IEquatable +public readonly struct DiagnosticSource : IEquatable { private readonly string? _value; - /// Initializes a new instance of the struct. - /// The value to associate with this . + /// Initializes a new instance of the struct. + /// The value to associate with this . [JsonConstructor] - public ProtocolStaticSectionAction(string value) + public DiagnosticSource(string value) { ArgumentException.ThrowIfNullOrWhiteSpace(value); _value = value; } - /// Gets the value associated with this . + /// Gets the value associated with this . public string Value => _value ?? string.Empty; - /// Replace the section content. - public static ProtocolStaticSectionAction Replace { get; } = new("replace"); - - /// Remove the section content. - public static ProtocolStaticSectionAction Remove { get; } = new("remove"); - - /// Append content to the section. - public static ProtocolStaticSectionAction Append { get; } = new("append"); - - /// Prepend content to the section. - public static ProtocolStaticSectionAction Prepend { get; } = new("prepend"); + /// Gets the mcp value. + public static DiagnosticSource Mcp { get; } = new("mcp"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(ProtocolStaticSectionAction left, ProtocolStaticSectionAction right) => left.Equals(right); + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(DiagnosticSource left, DiagnosticSource right) => left.Equals(right); - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(ProtocolStaticSectionAction left, ProtocolStaticSectionAction right) => !(left == right); + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(DiagnosticSource left, DiagnosticSource right) => !(left == right); /// - public override bool Equals(object? obj) => obj is ProtocolStaticSectionAction other && Equals(other); + public override bool Equals(object? obj) => obj is DiagnosticSource other && Equals(other); /// - public bool Equals(ProtocolStaticSectionAction other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + public bool Equals(DiagnosticSource other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); /// public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); @@ -36699,77 +37666,65 @@ public ProtocolStaticSectionAction(string value) /// public override string ToString() => Value; - /// Provides a for serializing instances. + /// Provides a for serializing instances. [EditorBrowsable(EditorBrowsableState.Never)] - public sealed class Converter : JsonConverter + public sealed class Converter : JsonConverter { /// - public override ProtocolStaticSectionAction Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override DiagnosticSource Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); } /// - public override void Write(Utf8JsonWriter writer, ProtocolStaticSectionAction value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, DiagnosticSource value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(ProtocolStaticSectionAction)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(DiagnosticSource)); } } } -/// The product serving the model, reported in telemetry as `model_provider`. +/// Availability of the EXPERIMENTAL session connector API. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct ProviderConfigModelProvider : IEquatable +public readonly struct ConnectorAvailability : IEquatable { private readonly string? _value; - /// Initializes a new instance of the struct. - /// The value to associate with this . + /// Initializes a new instance of the struct. + /// The value to associate with this . [JsonConstructor] - public ProviderConfigModelProvider(string value) + public ConnectorAvailability(string value) { ArgumentException.ThrowIfNullOrWhiteSpace(value); _value = value; } - /// Gets the value associated with this . + /// Gets the value associated with this . public string Value => _value ?? string.Empty; - /// OpenAI API. - public static ProviderConfigModelProvider Openai { get; } = new("openai"); - - /// Anthropic API. - public static ProviderConfigModelProvider Anthropic { get; } = new("anthropic"); - - /// Azure OpenAI Service. - public static ProviderConfigModelProvider AzureOpenai { get; } = new("azure_openai"); - - /// Ollama. - public static ProviderConfigModelProvider Ollama { get; } = new("ollama"); - - /// LM Studio. - public static ProviderConfigModelProvider LmStudio { get; } = new("lm_studio"); + /// The resolved Connector feature is enabled and Connector requests are permitted. + public static ConnectorAvailability Enabled { get; } = new("enabled"); - /// Foundry Local. - public static ProviderConfigModelProvider FoundryLocal { get; } = new("foundry_local"); + /// The resolved Connector feature is off. No Connector service request is made while disabled. + public static ConnectorAvailability Disabled { get; } = new("disabled"); - /// llama.cpp server. - public static ProviderConfigModelProvider LlamaCpp { get; } = new("llama_cpp"); + /// The session has no eligible host-owned GitHub account or does not support local Connector projection. + public static ConnectorAvailability Unavailable { get; } = new("unavailable"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(ProviderConfigModelProvider left, ProviderConfigModelProvider right) => left.Equals(right); + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(ConnectorAvailability left, ConnectorAvailability right) => left.Equals(right); - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(ProviderConfigModelProvider left, ProviderConfigModelProvider right) => !(left == right); + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(ConnectorAvailability left, ConnectorAvailability right) => !(left == right); /// - public override bool Equals(object? obj) => obj is ProviderConfigModelProvider other && Equals(other); + public override bool Equals(object? obj) => obj is ConnectorAvailability other && Equals(other); /// - public bool Equals(ProviderConfigModelProvider other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + public bool Equals(ConnectorAvailability other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); /// public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); @@ -36777,62 +37732,59 @@ public ProviderConfigModelProvider(string value) /// public override string ToString() => Value; - /// Provides a for serializing instances. + /// Provides a for serializing instances. [EditorBrowsable(EditorBrowsableState.Never)] - public sealed class Converter : JsonConverter + public sealed class Converter : JsonConverter { /// - public override ProviderConfigModelProvider Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override ConnectorAvailability Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); } /// - public override void Write(Utf8JsonWriter writer, ProviderConfigModelProvider value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, ConnectorAvailability value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(ProviderConfigModelProvider)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(ConnectorAvailability)); } } } -/// Provider transport. Defaults to "http". +/// Stable OAuth scope whose absence prevents Connector management. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct ProviderConfigTransport : IEquatable +public readonly struct ConnectorAuthorizationScope : IEquatable { private readonly string? _value; - /// Initializes a new instance of the struct. - /// The value to associate with this . + /// Initializes a new instance of the struct. + /// The value to associate with this . [JsonConstructor] - public ProviderConfigTransport(string value) + public ConnectorAuthorizationScope(string value) { ArgumentException.ThrowIfNullOrWhiteSpace(value); _value = value; } - /// Gets the value associated with this . + /// Gets the value associated with this . public string Value => _value ?? string.Empty; - /// HTTP request/streaming transport. - public static ProviderConfigTransport Http { get; } = new("http"); - - /// WebSocket transport. - public static ProviderConfigTransport Websockets { get; } = new("websockets"); + /// Allows Copilot to manage Connector connections and use their tools. + public static ConnectorAuthorizationScope WritePluginGatewayConnections { get; } = new("write_plugin_gateway_connections"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(ProviderConfigTransport left, ProviderConfigTransport right) => left.Equals(right); + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(ConnectorAuthorizationScope left, ConnectorAuthorizationScope right) => left.Equals(right); - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(ProviderConfigTransport left, ProviderConfigTransport right) => !(left == right); + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(ConnectorAuthorizationScope left, ConnectorAuthorizationScope right) => !(left == right); /// - public override bool Equals(object? obj) => obj is ProviderConfigTransport other && Equals(other); + public override bool Equals(object? obj) => obj is ConnectorAuthorizationScope other && Equals(other); /// - public bool Equals(ProviderConfigTransport other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + public bool Equals(ConnectorAuthorizationScope other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); /// public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); @@ -36840,65 +37792,71 @@ public ProviderConfigTransport(string value) /// public override string ToString() => Value; - /// Provides a for serializing instances. + /// Provides a for serializing instances. [EditorBrowsable(EditorBrowsableState.Never)] - public sealed class Converter : JsonConverter + public sealed class Converter : JsonConverter { /// - public override ProviderConfigTransport Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override ConnectorAuthorizationScope Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); } /// - public override void Write(Utf8JsonWriter writer, ProviderConfigTransport value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, ConnectorAuthorizationScope value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(ProviderConfigTransport)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(ConnectorAuthorizationScope)); } } } -/// Provider type. Defaults to "openai" for generic OpenAI-compatible APIs. +/// Authoritative service connection state for one Connector. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct ProviderConfigType : IEquatable +public readonly struct ConnectorCatalogStatus : IEquatable { private readonly string? _value; - /// Initializes a new instance of the struct. - /// The value to associate with this . + /// Initializes a new instance of the struct. + /// The value to associate with this . [JsonConstructor] - public ProviderConfigType(string value) + public ConnectorCatalogStatus(string value) { ArgumentException.ThrowIfNullOrWhiteSpace(value); _value = value; } - /// Gets the value associated with this . + /// Gets the value associated with this . public string Value => _value ?? string.Empty; - /// Generic OpenAI-compatible API. - public static ProviderConfigType Openai { get; } = new("openai"); + /// The Connector is available but not connected. + public static ConnectorCatalogStatus NotConnected { get; } = new("not_connected"); - /// Azure OpenAI Service endpoint. - public static ProviderConfigType Azure { get; } = new("azure"); + /// The Connector service is still completing connection or consent. + public static ConnectorCatalogStatus Pending { get; } = new("pending"); - /// Anthropic API endpoint. - public static ProviderConfigType Anthropic { get; } = new("anthropic"); + /// The Connector is connected and may contribute MCP servers. + public static ConnectorCatalogStatus Connected { get; } = new("connected"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(ProviderConfigType left, ProviderConfigType right) => left.Equals(right); + /// The Connector service reports an unusable connection. + public static ConnectorCatalogStatus Error { get; } = new("error"); - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(ProviderConfigType left, ProviderConfigType right) => !(left == right); + /// The service returned a future or unrecognized state. + public static ConnectorCatalogStatus Unknown { get; } = new("unknown"); + + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(ConnectorCatalogStatus left, ConnectorCatalogStatus right) => left.Equals(right); + + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(ConnectorCatalogStatus left, ConnectorCatalogStatus right) => !(left == right); /// - public override bool Equals(object? obj) => obj is ProviderConfigType other && Equals(other); + public override bool Equals(object? obj) => obj is ConnectorCatalogStatus other && Equals(other); /// - public bool Equals(ProviderConfigType other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + public bool Equals(ConnectorCatalogStatus other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); /// public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); @@ -36906,62 +37864,77 @@ public ProviderConfigType(string value) /// public override string ToString() => Value; - /// Provides a for serializing instances. + /// Provides a for serializing instances. [EditorBrowsable(EditorBrowsableState.Never)] - public sealed class Converter : JsonConverter + public sealed class Converter : JsonConverter { /// - public override ProviderConfigType Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override ConnectorCatalogStatus Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); } /// - public override void Write(Utf8JsonWriter writer, ProviderConfigType value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, ConnectorCatalogStatus value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(ProviderConfigType)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(ConnectorCatalogStatus)); } } } -/// Wire API format (openai/azure only). Defaults to "completions". +/// Live MCP status of one Connector-owned runtime server. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct ProviderConfigWireApi : IEquatable +public readonly struct ConnectorMcpStatus : IEquatable { private readonly string? _value; - /// Initializes a new instance of the struct. - /// The value to associate with this . + /// Initializes a new instance of the struct. + /// The value to associate with this . [JsonConstructor] - public ProviderConfigWireApi(string value) + public ConnectorMcpStatus(string value) { ArgumentException.ThrowIfNullOrWhiteSpace(value); _value = value; } - /// Gets the value associated with this . + /// Gets the value associated with this . public string Value => _value ?? string.Empty; - /// OpenAI Chat Completions wire format. - public static ProviderConfigWireApi Completions { get; } = new("completions"); + /// The server is connected and its tools are available. + public static ConnectorMcpStatus Connected { get; } = new("connected"); - /// OpenAI Responses API wire format. - public static ProviderConfigWireApi Responses { get; } = new("responses"); + /// The server connection is still being established. + public static ConnectorMcpStatus Pending { get; } = new("pending"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(ProviderConfigWireApi left, ProviderConfigWireApi right) => left.Equals(right); + /// The server requires refreshed GitHub authorization. + public static ConnectorMcpStatus NeedsAuth { get; } = new("needs_auth"); - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(ProviderConfigWireApi left, ProviderConfigWireApi right) => !(left == right); + /// The server failed to connect or initialize. + public static ConnectorMcpStatus Failed { get; } = new("failed"); + + /// The server is intentionally stopped, including when managed policy blocks it. + public static ConnectorMcpStatus Stopped { get; } = new("stopped"); + + /// The server is configured but explicitly disabled. + public static ConnectorMcpStatus Disabled { get; } = new("disabled"); + + /// The Connector currently has no live server configuration. + public static ConnectorMcpStatus NotConfigured { get; } = new("not_configured"); + + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(ConnectorMcpStatus left, ConnectorMcpStatus right) => left.Equals(right); + + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(ConnectorMcpStatus left, ConnectorMcpStatus right) => !(left == right); /// - public override bool Equals(object? obj) => obj is ProviderConfigWireApi other && Equals(other); + public override bool Equals(object? obj) => obj is ConnectorMcpStatus other && Equals(other); /// - public bool Equals(ProviderConfigWireApi other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + public bool Equals(ConnectorMcpStatus other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); /// public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); @@ -36969,20 +37942,20 @@ public ProviderConfigWireApi(string value) /// public override string ToString() => Value; - /// Provides a for serializing instances. + /// Provides a for serializing instances. [EditorBrowsable(EditorBrowsableState.Never)] - public sealed class Converter : JsonConverter + public sealed class Converter : JsonConverter { /// - public override ProviderConfigWireApi Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override ConnectorMcpStatus Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); } /// - public override void Write(Utf8JsonWriter writer, ProviderConfigWireApi value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, ConnectorMcpStatus value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(ProviderConfigWireApi)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(ConnectorMcpStatus)); } } } @@ -41154,12 +42127,6 @@ public async Task RegisterExtensionLaunchProviderAsync(CancellationToken cancell field ?? Interlocked.CompareExchange(ref field, new(_rpc), null) ?? field; - - /// Accounts APIs. - public ServerAccountsApi Accounts => - field ?? - Interlocked.CompareExchange(ref field, new(_rpc), null) ?? - field; } /// Provides server-scoped Environments APIs. @@ -41390,6 +42357,72 @@ public async Task GetHostSupportAsync(CancellationToken canc { return await CopilotClient.InvokeRpcAsync(_rpc, "sandbox.getHostSupport", [], cancellationToken); } + + /// ProxyCa APIs. + public ServerSandboxProxyCaApi ProxyCa => + field ?? + Interlocked.CompareExchange(ref field, new(_rpc), null) ?? + field; +} + +/// Provides server-scoped SandboxProxyCa APIs. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class ServerSandboxProxyCaApi +{ + private readonly JsonRpc _rpc; + + internal ServerSandboxProxyCaApi(JsonRpc rpc) + { + _rpc = rpc; + } + + /// Reports whether the persistent certificate authority of the sandbox credential proxy exists, whether OS trust includes it, and whether it must be rotated. Changes nothing. + /// The sandbox configuration that the host gives its sessions. The runtime reads the credential hosts from `auth` and `credentials`; it ignores `enabled` and the other fields. + /// The to monitor for cancellation requests. The default is . + /// Status of the persistent certificate authority of the sandbox credential proxy. + public async Task GetStatusAsync(SandboxConfig? sandboxConfig = null, CancellationToken cancellationToken = default) + { + var request = new SandboxProxyCaRequest { SandboxConfig = sandboxConfig }; + return await CopilotClient.InvokeRpcAsync(_rpc, "sandbox.proxyCa.getStatus", [request], cancellationToken); + } + + /// Creates the persistent certificate authority of the sandbox credential proxy if none is stored, without changing OS trust, and returns the path of its public certificate. Keeps an existing certificate authority, even one that must be rotated. Fails where OS trust is unsupported. Trust it with sandbox.proxyCa.trust: the CLI trusts only the hosts in the saved user settings, so it refuses a certificate authority that also covers hosts from sandboxConfig. + /// The sandbox configuration that the host gives its sessions. The runtime reads the credential hosts from `auth` and `credentials`; it ignores `enabled` and the other fields. + /// The to monitor for cancellation requests. The default is . + /// Result of creating the persistent certificate authority of the sandbox credential proxy. + public async Task CreateAsync(SandboxConfig? sandboxConfig = null, CancellationToken cancellationToken = default) + { + var request = new SandboxProxyCaRequest { SandboxConfig = sandboxConfig }; + return await CopilotClient.InvokeRpcAsync(_rpc, "sandbox.proxyCa.create", [request], cancellationToken); + } + + /// Replaces the persistent certificate authority of the sandbox credential proxy with a new one for the current credential hosts. If OS trust included the old one, removes it and trusts the new one, which can show an OS authentication prompt. Running sandboxed tools keep the old certificate authority until they restart. + /// The sandbox configuration that the host gives its sessions. The runtime reads the credential hosts from `auth` and `credentials`; it ignores `enabled` and the other fields. + /// The to monitor for cancellation requests. The default is . + /// Status of the persistent certificate authority of the sandbox credential proxy. + public async Task RotateAsync(SandboxConfig? sandboxConfig = null, CancellationToken cancellationToken = default) + { + var request = new SandboxProxyCaRequest { SandboxConfig = sandboxConfig }; + return await CopilotClient.InvokeRpcAsync(_rpc, "sandbox.proxyCa.rotate", [request], cancellationToken); + } + + /// Adds the persistent certificate authority of the sandbox credential proxy to OS trust, so sandboxed clients that read only OS trust accept the proxy. Call create first. Refuses a certificate authority that is not constrained to the current credential hosts. Can show an OS authentication prompt. + /// The sandbox configuration that the host gives its sessions. The runtime reads the credential hosts from `auth` and `credentials`; it ignores `enabled` and the other fields. + /// The to monitor for cancellation requests. The default is . + /// Status of the persistent certificate authority of the sandbox credential proxy. + public async Task TrustAsync(SandboxConfig? sandboxConfig = null, CancellationToken cancellationToken = default) + { + var request = new SandboxProxyCaRequest { SandboxConfig = sandboxConfig }; + return await CopilotClient.InvokeRpcAsync(_rpc, "sandbox.proxyCa.trust", [request], cancellationToken); + } + + /// Removes the persistent certificate authority of the sandbox credential proxy from OS trust. Keeps the stored certificate authority. Can show an OS authentication prompt. Sandboxed clients that read only OS trust then reject the proxy; clients that read the per-process certificate bundle continue to work. + /// The to monitor for cancellation requests. The default is . + /// Status of the persistent certificate authority of the sandbox credential proxy. + public async Task RemoveAsync(CancellationToken cancellationToken = default) + { + return await CopilotClient.InvokeRpcAsync(_rpc, "sandbox.proxyCa.remove", [], cancellationToken); + } } /// Provides server-scoped Tools APIs. @@ -43059,38 +44092,6 @@ public async Task SpawnAsync(string cwd, string? agent } } -/// Provides server-scoped Accounts APIs. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class ServerAccountsApi -{ - private readonly JsonRpc _rpc; - - internal ServerAccountsApi(JsonRpc rpc) - { - _rpc = rpc; - } - - /// Acquire a Microsoft Entra access token through the runtime's OneAuth broker. Account-scoped because it uses the same native broker as the account stack: a trusted host application mints a scoped Entra token for its own use, most notably to authenticate to a remote MCP server whose authorization server is Entra ID (in place of the generic browser-OAuth flow). - /// Public client application id. - /// Tenant id or tenant selector, such as common or organizations. - /// Broker redirect URI registered for the client. Required: the OneAuth broker validates a non-empty, registered redirect URI for the public client (MSAL broker registration), so this is not a browser-flow vestige and cannot be omitted. - /// Exact delegated scopes to request. - /// Whether the broker may show interaction. - /// Previously rejected token that OneAuth must bypass during renewal. - /// The to monitor for cancellation requests. The default is . - /// Result of a OneAuth token acquisition. - internal async Task AcquireEntraTokenAsync(string clientId, string tenantId, string redirectUri, IList scopes, EntraTokenInteraction interaction, string? accessTokenToRenew = null, CancellationToken cancellationToken = default) - { - ArgumentNullException.ThrowIfNull(clientId); - ArgumentNullException.ThrowIfNull(tenantId); - ArgumentNullException.ThrowIfNull(redirectUri); - ArgumentNullException.ThrowIfNull(scopes); - - var request = new EntraTokenAcquireRequest { ClientId = clientId, TenantId = tenantId, RedirectUri = redirectUri, Scopes = scopes, Interaction = interaction, AccessTokenToRenew = accessTokenToRenew }; - return await CopilotClient.InvokeRpcAsync(_rpc, "accounts.acquireEntraToken", [request], cancellationToken); - } -} - /// Provides typed session-scoped RPC methods. public sealed class SessionRpc { @@ -43103,6 +44104,12 @@ internal SessionRpc(CopilotSession session) internal CopilotSession Session => _session; + /// Providers APIs. + public ProvidersApi Providers => + field ?? + Interlocked.CompareExchange(ref field, new(_session), null) ?? + field; + /// Sandbox APIs. public SandboxApi Sandbox => field ?? @@ -43520,6 +44527,102 @@ public async Task LogAsync(string message, SessionLogLevel? level = n } } +/// Provides session-scoped Providers APIs. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class ProvidersApi +{ + private readonly CopilotSession _session; + + internal ProvidersApi(CopilotSession session) + { + _session = session; + } + + /// Returns adapter definitions and supported operations in this session's effective provider catalog, without running discovery. Does not list provider instances or select inference models. + /// The to monitor for cancellation requests. The default is . + /// Normalized model-provider adapter definitions available to the session, not discovered instances. + public async Task GetCatalogAsync(CancellationToken cancellationToken = default) + { + _session.ThrowIfDisposed(); + + var request = new SessionProvidersGetCatalogRequest { SessionId = _session.SessionId }; + return await CopilotClient.InvokeRpcAsync(_session.Rpc, "session.providers.getCatalog", [request], cancellationToken); + } + + /// Discovers reachable instances using an adapter from this session's effective provider catalog and provider-specific discovery input. + /// Opaque adapter identity returned by `session.providers.getCatalog`. + /// Provider-specific JSON input. Omission or null selects adapter defaults unless requiresInput is true. Non-null input is validated against the advertised Draft 7 schema when present; otherwise validation belongs to the adapter. + /// The to monitor for cancellation requests. The default is . + /// Provider instances found by a discovery operation. + public async Task DiscoverAsync(string adapterId, object? input = null, CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(adapterId); + _session.ThrowIfDisposed(); + + var request = new ModelProviderDiscoverRequest { SessionId = _session.SessionId, AdapterId = adapterId, Input = CopilotClient.ToJsonElementForWire(input) }; + return await CopilotClient.InvokeRpcAsync(_session.Rpc, "session.providers.discover", [request], cancellationToken); + } + + /// Gets current health and version information for a discovered model-provider instance. + /// Provider instance reference returned by discovery. + /// The to monitor for cancellation requests. The default is . + /// Current health information for a provider instance. + public async Task GetStatusAsync(ModelProviderInstanceReference instance, CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(instance); + _session.ThrowIfDisposed(); + + var request = new ModelProviderGetStatusRequest { SessionId = _session.SessionId, Instance = instance }; + return await CopilotClient.InvokeRpcAsync(_session.Rpc, "session.providers.getStatus", [request], cancellationToken); + } + + /// Models APIs. + public ProvidersModelsApi Models => + field ?? + Interlocked.CompareExchange(ref field, new(_session), null) ?? + field; +} + +/// Provides session-scoped ProvidersModels APIs. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class ProvidersModelsApi +{ + private readonly CopilotSession _session; + + internal ProvidersModelsApi(CopilotSession session) + { + _session = session; + } + + /// Lists models installed or otherwise available from a discovered model-provider instance. + /// Provider instance reference returned by discovery. + /// The to monitor for cancellation requests. The default is . + /// Models offered for agent conversations by one provider instance. Adapters exclude known-incompatible models, but retain candidates with unknown capabilities. Listing does not guarantee compatibility. + public async Task ListAsync(ModelProviderInstanceReference instance, CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(instance); + _session.ThrowIfDisposed(); + + var request = new ModelProviderModelsListRequest { SessionId = _session.SessionId, Instance = instance }; + return await CopilotClient.InvokeRpcAsync(_session.Rpc, "session.providers.models.list", [request], cancellationToken); + } + + /// Translates a discovered model into the provider and model configuration needed to use it, and reports whether each is already registered in this session. Prepares only: it registers nothing, writes nothing, and performs no provider requests. + /// The discovered instance that serves the model. + /// The discovered model to configure. + /// The to monitor for cancellation requests. The default is . + /// Provider configuration prepared from a discovered model. Preparing a plan changes nothing: it neither registers the model with the session nor writes durable configuration. To apply it, pass `provider` and `model` to `session.provider.add`, omitting whichever the dispositions report as already configured. + public async Task PrepareConfigurationAsync(ModelProviderInstance instance, DiscoveredModel model, CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(instance); + ArgumentNullException.ThrowIfNull(model); + _session.ThrowIfDisposed(); + + var request = new ModelProviderPrepareConfigurationRequest { SessionId = _session.SessionId, Instance = instance, Model = model }; + return await CopilotClient.InvokeRpcAsync(_session.Rpc, "session.providers.models.prepareConfiguration", [request], cancellationToken); + } +} + /// Provides session-scoped Sandbox APIs. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] public sealed class SandboxApi @@ -44875,14 +45978,15 @@ public async Task GetSourcesAsync(CancellationToke return await CopilotClient.InvokeRpcAsync(_session.Rpc, "session.instructions.getSources", [request], cancellationToken); } - /// Invalidates cached custom-instruction discovery so subsequent turns and source reads observe instruction files currently on disk. + /// For local sessions, invalidates instruction discovery and the model-facing prompt, then returns freshly discovered sources. The updated prompt takes effect on the next turn. Remote sessions must reload on their agent host instead. /// The to monitor for cancellation requests. The default is . - public async Task ReloadAsync(CancellationToken cancellationToken = default) + /// Instruction sources loaded for the session, in merge order. + public async Task ReloadAsync(CancellationToken cancellationToken = default) { _session.ThrowIfDisposed(); var request = new SessionInstructionsReloadRequest { SessionId = _session.SessionId }; - await CopilotClient.InvokeRpcAsync(_session.Rpc, "session.instructions.reload", [request], cancellationToken); + return await CopilotClient.InvokeRpcAsync(_session.Rpc, "session.instructions.reload", [request], cancellationToken); } } @@ -44897,15 +46001,15 @@ internal CustomizationsApi(CopilotSession session) _session = session; } - /// Reloads all repository and user customizations for the active session: instructions, plugins and their MCP servers and hooks, custom agents, extensions, and skills. Returns diagnostics from the final skill reload. + /// For local sessions, reconciles repository context and discovered instructions, plugins, skills, agents, hooks, MCP servers, and extensions after files appear or change under the working directory. Independent component failures are returned in outcomes and errors; a rejected call can have partially applied earlier steps. Remote sessions must reload on their agent host instead. The model-facing context is rebuilt on the next turn. /// The to monitor for cancellation requests. The default is . - /// Diagnostics from reloading skill definitions, with warnings and errors as separate lists. - public async Task ReloadAsync(CancellationToken cancellationToken = default) + /// Results of reloading discovered session customizations. Inspect outcomes for reloaded, skipped, or failed subsystems; a rejection may follow partial mutation. Changes to the model-facing prompt and tools apply on the next turn. + public async Task ReloadAsync(CancellationToken cancellationToken = default) { _session.ThrowIfDisposed(); var request = new SessionCustomizationsReloadRequest { SessionId = _session.SessionId }; - return await CopilotClient.InvokeRpcAsync(_session.Rpc, "session.customizations.reload", [request], cancellationToken); + return await CopilotClient.InvokeRpcAsync(_session.Rpc, "session.customizations.reload", [request], cancellationToken); } } @@ -46981,6 +48085,21 @@ public async Task HandlePendingElicitationAsync(string requ return await CopilotClient.InvokeRpcAsync(_session.Rpc, "session.ui.handlePendingElicitation", [request], cancellationToken); } + /// Resolves a pending elicitation request after direct interaction in the trusted in-process client. Only an accepted response to the built-in ask_user tool can become trusted human evidence. + /// The unique request ID from the elicitation.requested event. + /// The elicitation response (accept with form values, decline, or cancel). + /// The to monitor for cancellation requests. The default is . + /// Indicates whether the elicitation response was accepted; false if it was already resolved by another client. + internal async Task HandleHumanAskUserAsync(string requestId, UIElicitationResponse result, CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(requestId); + ArgumentNullException.ThrowIfNull(result); + _session.ThrowIfDisposed(); + + var request = new UIHandlePendingElicitationRequest { SessionId = _session.SessionId, RequestId = requestId, Result = result }; + return await CopilotClient.InvokeRpcAsync(_session.Rpc, "session.ui.handleHumanAskUser", [request], cancellationToken); + } + /// Resolves a pending `user_input.requested` event with the user's response. /// The unique request ID from the user_input.requested event. /// User response for a pending user-input request, with answer text and whether it was typed freeform. @@ -46996,6 +48115,21 @@ public async Task HandlePendingUserInputAsync(string requ return await CopilotClient.InvokeRpcAsync(_session.Rpc, "session.ui.handlePendingUserInput", [request], cancellationToken); } + /// Resolves a pending `user_input.requested` event after direct interaction in the trusted in-process client. + /// The unique request ID from the user_input.requested event. + /// User response for a pending user-input request, with answer text and whether it was typed freeform. + /// The to monitor for cancellation requests. The default is . + /// Indicates whether the pending UI request was resolved by this call. + internal async Task HandleHumanUserInputAsync(string requestId, UIUserInputResponse response, CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(requestId); + ArgumentNullException.ThrowIfNull(response); + _session.ThrowIfDisposed(); + + var request = new UIHandlePendingUserInputRequest { SessionId = _session.SessionId, RequestId = requestId, Response = response }; + return await CopilotClient.InvokeRpcAsync(_session.Rpc, "session.ui.handleHumanUserInput", [request], cancellationToken); + } + /// Resolves a pending `sampling.requested` event with a sampling result, or rejects it. /// The unique request ID from the sampling.requested event. /// Optional sampling result payload. Omit to reject/cancel the sampling request without providing a result. @@ -47054,6 +48188,21 @@ public async Task HandlePendingExitPlanModeAsync(string r return await CopilotClient.InvokeRpcAsync(_session.Rpc, "session.ui.handlePendingExitPlanMode", [request], cancellationToken); } + /// Resolves a pending `exit_plan_mode.requested` event after direct interaction in the trusted in-process client. + /// The unique request ID from the exit_plan_mode.requested event. + /// User response for a pending exit-plan-mode request, with approval state, selected action, auto-approve flag, and feedback. + /// The to monitor for cancellation requests. The default is . + /// Indicates whether the pending UI request was resolved by this call. + internal async Task HandleHumanExitPlanModeAsync(string requestId, UIExitPlanModeResponse response, CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(requestId); + ArgumentNullException.ThrowIfNull(response); + _session.ThrowIfDisposed(); + + var request = new UIHandlePendingExitPlanModeRequest { SessionId = _session.SessionId, RequestId = requestId, Response = response }; + return await CopilotClient.InvokeRpcAsync(_session.Rpc, "session.ui.handleHumanExitPlanMode", [request], cancellationToken); + } + /// Registers an in-process handler for auto-mode-switch requests so the server bridge skips dispatch. /// The to monitor for cancellation requests. The default is . /// Register an in-process handler for `auto_mode_switch.requested` events. The caller still attaches the actual listener via the standard event-subscription mechanism; this registration solely tells the server bridge to skip its own dispatch (so a remote client doesn't race the in-process handler for the same requestId). @@ -48480,11 +49629,21 @@ public interface ISessionFsHandler /// The to monitor for cancellation requests. The default is . /// File content as a UTF-8 string, or a filesystem error if the read failed. Task ReadFileAsync(SessionFsReadFileRequest request, CancellationToken cancellationToken = default); + /// Reads binary file content from the client-provided session filesystem. + /// Path of the binary file to read from the client-provided session filesystem. + /// The to monitor for cancellation requests. The default is . + /// File bytes as standard base64, or a filesystem error if the read failed. + Task ReadFileBytesAsync(SessionFsReadFileBytesRequest request, CancellationToken cancellationToken = default); /// Writes a file in the client-provided session filesystem. /// File path, content to write, and optional mode for the client-provided session filesystem. /// The to monitor for cancellation requests. The default is . /// Describes a filesystem error. Task WriteFileAsync(SessionFsWriteFileRequest request, CancellationToken cancellationToken = default); + /// Writes binary file content to the client-provided session filesystem. + /// File path, standard-base64-encoded bytes to write, and optional mode for the client-provided session filesystem. + /// The to monitor for cancellation requests. The default is . + /// Describes a filesystem error. + Task WriteFileBytesAsync(SessionFsWriteFileBytesRequest request, CancellationToken cancellationToken = default); /// Appends content to a file in the client-provided session filesystem, creating parent directories as needed. /// File path, content to append, and optional mode for the client-provided session filesystem. Implementations create parent directories as needed. /// The to monitor for cancellation requests. The default is . @@ -48621,12 +49780,24 @@ public static void RegisterClientSessionApiHandlers(JsonRpc rpc, Func>)(async (request, cancellationToken) => + { + var handler = getHandlers(request.SessionId).SessionFs; + if (handler is null) throw new InvalidOperationException($"No sessionFs handler registered for session: {request.SessionId}"); + return await handler.ReadFileBytesAsync(request, cancellationToken); + }), singleObjectParam: true); rpc.SetLocalRpcMethod("sessionFs.writeFile", (Func>)(async (request, cancellationToken) => { var handler = getHandlers(request.SessionId).SessionFs; if (handler is null) throw new InvalidOperationException($"No sessionFs handler registered for session: {request.SessionId}"); return await handler.WriteFileAsync(request, cancellationToken); }), singleObjectParam: true); + rpc.SetLocalRpcMethod("sessionFs.writeFileBytes", (Func>)(async (request, cancellationToken) => + { + var handler = getHandlers(request.SessionId).SessionFs; + if (handler is null) throw new InvalidOperationException($"No sessionFs handler registered for session: {request.SessionId}"); + return await handler.WriteFileBytesAsync(request, cancellationToken); + }), singleObjectParam: true); rpc.SetLocalRpcMethod("sessionFs.appendFile", (Func>)(async (request, cancellationToken) => { var handler = getHandlers(request.SessionId).SessionFs; @@ -49031,6 +50202,10 @@ public static void RegisterClientGlobalApiHandlers(JsonRpc rpc, ClientGlobalApiH [JsonSerializable(typeof(GitHub.Copilot.HookProgressEvent), TypeInfoPropertyName = "SessionEventsHookProgressEvent")] [JsonSerializable(typeof(GitHub.Copilot.HookStartData), TypeInfoPropertyName = "SessionEventsHookStartData")] [JsonSerializable(typeof(GitHub.Copilot.HookStartEvent), TypeInfoPropertyName = "SessionEventsHookStartEvent")] +[JsonSerializable(typeof(GitHub.Copilot.HumanResponseActor), TypeInfoPropertyName = "SessionEventsHumanResponseActor")] +[JsonSerializable(typeof(GitHub.Copilot.HumanResponseRecordedData), TypeInfoPropertyName = "SessionEventsHumanResponseRecordedData")] +[JsonSerializable(typeof(GitHub.Copilot.HumanResponseRecordedEvent), TypeInfoPropertyName = "SessionEventsHumanResponseRecordedEvent")] +[JsonSerializable(typeof(GitHub.Copilot.HumanResponseRecordedResponse), TypeInfoPropertyName = "SessionEventsHumanResponseRecordedResponse")] [JsonSerializable(typeof(GitHub.Copilot.IndexedSearchDisabledReason), TypeInfoPropertyName = "SessionEventsIndexedSearchDisabledReason")] [JsonSerializable(typeof(GitHub.Copilot.IndexedSearchErrorType), TypeInfoPropertyName = "SessionEventsIndexedSearchErrorType")] [JsonSerializable(typeof(GitHub.Copilot.IndexedSearchIncrementalPhase), TypeInfoPropertyName = "SessionEventsIndexedSearchIncrementalPhase")] @@ -49266,6 +50441,8 @@ public static void RegisterClientGlobalApiHandlers(JsonRpc rpc, ClientGlobalApiH [JsonSerializable(typeof(GitHub.Copilot.ToolExecutionCompleteData), TypeInfoPropertyName = "SessionEventsToolExecutionCompleteData")] [JsonSerializable(typeof(GitHub.Copilot.ToolExecutionCompleteError), TypeInfoPropertyName = "SessionEventsToolExecutionCompleteError")] [JsonSerializable(typeof(GitHub.Copilot.ToolExecutionCompleteEvent), TypeInfoPropertyName = "SessionEventsToolExecutionCompleteEvent")] +[JsonSerializable(typeof(GitHub.Copilot.ToolExecutionCompleteFileEdit), TypeInfoPropertyName = "SessionEventsToolExecutionCompleteFileEdit")] +[JsonSerializable(typeof(GitHub.Copilot.ToolExecutionCompleteFileEditKind), TypeInfoPropertyName = "SessionEventsToolExecutionCompleteFileEditKind")] [JsonSerializable(typeof(GitHub.Copilot.ToolExecutionCompleteResult), TypeInfoPropertyName = "SessionEventsToolExecutionCompleteResult")] [JsonSerializable(typeof(GitHub.Copilot.ToolExecutionCompleteShellExecution), TypeInfoPropertyName = "SessionEventsToolExecutionCompleteShellExecution")] [JsonSerializable(typeof(GitHub.Copilot.ToolExecutionCompleteToolDescription), TypeInfoPropertyName = "SessionEventsToolExecutionCompleteToolDescription")] @@ -49293,6 +50470,9 @@ public static void RegisterClientGlobalApiHandlers(JsonRpc rpc, ClientGlobalApiH [JsonSerializable(typeof(GitHub.Copilot.ToolExecutionStartToolDescriptionMetaUIVisibility), TypeInfoPropertyName = "SessionEventsToolExecutionStartToolDescriptionMetaUIVisibility")] [JsonSerializable(typeof(GitHub.Copilot.ToolSearchActivatedData), TypeInfoPropertyName = "SessionEventsToolSearchActivatedData")] [JsonSerializable(typeof(GitHub.Copilot.ToolSearchActivatedEvent), TypeInfoPropertyName = "SessionEventsToolSearchActivatedEvent")] +[JsonSerializable(typeof(GitHub.Copilot.ToolShellOutputData), TypeInfoPropertyName = "SessionEventsToolShellOutputData")] +[JsonSerializable(typeof(GitHub.Copilot.ToolShellOutputEvent), TypeInfoPropertyName = "SessionEventsToolShellOutputEvent")] +[JsonSerializable(typeof(GitHub.Copilot.ToolShellOutputStream), TypeInfoPropertyName = "SessionEventsToolShellOutputStream")] [JsonSerializable(typeof(GitHub.Copilot.ToolUserRequestedData), TypeInfoPropertyName = "SessionEventsToolUserRequestedData")] [JsonSerializable(typeof(GitHub.Copilot.ToolUserRequestedEvent), TypeInfoPropertyName = "SessionEventsToolUserRequestedEvent")] [JsonSerializable(typeof(GitHub.Copilot.UIEphemeralQueryPhase), TypeInfoPropertyName = "SessionEventsUIEphemeralQueryPhase")] @@ -49467,6 +50647,8 @@ public static void RegisterClientGlobalApiHandlers(JsonRpc rpc, ClientGlobalApiH [JsonSerializable(typeof(CopilotUserResponseQuotaSnapshotsPremiumInteractions))] [JsonSerializable(typeof(CurrentModel))] [JsonSerializable(typeof(CurrentToolMetadata))] +[JsonSerializable(typeof(CustomizationReloadOutcome))] +[JsonSerializable(typeof(CustomizationsReloadResult))] [JsonSerializable(typeof(DebugCollectLogsCollectedEntry))] [JsonSerializable(typeof(DebugCollectLogsDestination))] [JsonSerializable(typeof(DebugCollectLogsEntry))] @@ -49488,10 +50670,10 @@ public static void RegisterClientGlobalApiHandlers(JsonRpc rpc, ClientGlobalApiH [JsonSerializable(typeof(DiscoveredExtensionsEnableRequest))] [JsonSerializable(typeof(DiscoveredHook))] [JsonSerializable(typeof(DiscoveredMcpServer))] +[JsonSerializable(typeof(DiscoveredModel))] +[JsonSerializable(typeof(DiscoveredModelList))] [JsonSerializable(typeof(EnqueueCommandParams))] [JsonSerializable(typeof(EnqueueCommandResult))] -[JsonSerializable(typeof(EntraTokenAcquireRequest))] -[JsonSerializable(typeof(EntraTokenAcquireResult))] [JsonSerializable(typeof(EnvironmentCapabilities))] [JsonSerializable(typeof(EnvironmentsDeleteRequest))] [JsonSerializable(typeof(EnvironmentsDeleteResult))] @@ -49781,6 +50963,7 @@ public static void RegisterClientGlobalApiHandlers(JsonRpc rpc, ClientGlobalApiH [JsonSerializable(typeof(ModeSetResult))] [JsonSerializable(typeof(Model))] [JsonSerializable(typeof(ModelApplyStartupOverlayRequest))] +[JsonSerializable(typeof(ModelArtifactDetails))] [JsonSerializable(typeof(ModelBilling))] [JsonSerializable(typeof(ModelBillingPromo))] [JsonSerializable(typeof(ModelBillingTokenPrices))] @@ -49799,8 +50982,25 @@ public static void RegisterClientGlobalApiHandlers(JsonRpc rpc, ClientGlobalApiH [JsonSerializable(typeof(ModelPickerSettingsContext))] [JsonSerializable(typeof(ModelPickerSettingsContextEnvironment))] [JsonSerializable(typeof(ModelPolicy))] +[JsonSerializable(typeof(ModelProviderAdapterCatalog))] +[JsonSerializable(typeof(ModelProviderAdapterDescriptor))] +[JsonSerializable(typeof(ModelProviderAdapterOperationDescriptor))] +[JsonSerializable(typeof(ModelProviderAttribution))] +[JsonSerializable(typeof(ModelProviderAutomaticDiscoveryPolicy))] +[JsonSerializable(typeof(ModelProviderConfigurationPlan))] [JsonSerializable(typeof(ModelProviderDescriptor))] +[JsonSerializable(typeof(ModelProviderDiscoverRequest))] +[JsonSerializable(typeof(ModelProviderDiscoverResult))] +[JsonSerializable(typeof(ModelProviderGetStatusRequest))] +[JsonSerializable(typeof(ModelProviderInstance))] +[JsonSerializable(typeof(ModelProviderInstanceReference))] +[JsonSerializable(typeof(ModelProviderModelsListRequest))] +[JsonSerializable(typeof(ModelProviderOperationOutcome))] +[JsonSerializable(typeof(ModelProviderPrepareConfigurationRequest))] +[JsonSerializable(typeof(ModelProviderProvenance))] [JsonSerializable(typeof(ModelProviderRef))] +[JsonSerializable(typeof(ModelProviderStatus))] +[JsonSerializable(typeof(ModelProviderWarning))] [JsonSerializable(typeof(ModelSetAllowedModelsRequest))] [JsonSerializable(typeof(ModelSetAllowedModelsResult))] [JsonSerializable(typeof(ModelSetReasoningEffortRequest))] @@ -49998,6 +51198,9 @@ public static void RegisterClientGlobalApiHandlers(JsonRpc rpc, ClientGlobalApiH [JsonSerializable(typeof(SandboxHostCapability))] [JsonSerializable(typeof(SandboxHostSupport))] [JsonSerializable(typeof(SandboxMaskedEnvVar))] +[JsonSerializable(typeof(SandboxProxyCaCreateResult))] +[JsonSerializable(typeof(SandboxProxyCaRequest))] +[JsonSerializable(typeof(SandboxProxyCaStatus))] [JsonSerializable(typeof(ScheduleAddAtRequest))] [JsonSerializable(typeof(ScheduleAddCronRequest))] [JsonSerializable(typeof(ScheduleAddRequest))] @@ -50056,6 +51259,8 @@ public static void RegisterClientGlobalApiHandlers(JsonRpc rpc, ClientGlobalApiH [JsonSerializable(typeof(SessionFsExistsRequest))] [JsonSerializable(typeof(SessionFsExistsResult))] [JsonSerializable(typeof(SessionFsMkdirRequest))] +[JsonSerializable(typeof(SessionFsReadFileBytesRequest))] +[JsonSerializable(typeof(SessionFsReadFileBytesResult))] [JsonSerializable(typeof(SessionFsReadFileRequest))] [JsonSerializable(typeof(SessionFsReadFileResult))] [JsonSerializable(typeof(SessionFsReaddirRequest))] @@ -50078,6 +51283,7 @@ public static void RegisterClientGlobalApiHandlers(JsonRpc rpc, ClientGlobalApiH [JsonSerializable(typeof(SessionFsSqliteTransactionStatement))] [JsonSerializable(typeof(SessionFsStatRequest))] [JsonSerializable(typeof(SessionFsStatResult))] +[JsonSerializable(typeof(SessionFsWriteFileBytesRequest))] [JsonSerializable(typeof(SessionFsWriteFileRequest))] [JsonSerializable(typeof(SessionGitHubAuthGetAllAuthAvailableRequest))] [JsonSerializable(typeof(SessionGitHubAuthGetCurrentAuthInfoRequest))] @@ -50144,6 +51350,7 @@ public static void RegisterClientGlobalApiHandlers(JsonRpc rpc, ClientGlobalApiH [JsonSerializable(typeof(SessionPluginsReloadRequestWithSession))] [JsonSerializable(typeof(SessionProviderGetEndpointRequest))] [JsonSerializable(typeof(SessionProviderGetEndpointRequestWithSession))] +[JsonSerializable(typeof(SessionProvidersGetCatalogRequest))] [JsonSerializable(typeof(SessionPruneResult))] [JsonSerializable(typeof(SessionQueueClearRequest))] [JsonSerializable(typeof(SessionQueueEnqueueResumePendingRequest))] diff --git a/dotnet/src/Generated/SessionEvents.cs b/dotnet/src/Generated/SessionEvents.cs index 6f79c38d66..c482a8a69f 100644 --- a/dotnet/src/Generated/SessionEvents.cs +++ b/dotnet/src/Generated/SessionEvents.cs @@ -61,6 +61,7 @@ namespace GitHub.Copilot; [JsonDerivedType(typeof(HookEndEvent), "hook.end")] [JsonDerivedType(typeof(HookProgressEvent), "hook.progress")] [JsonDerivedType(typeof(HookStartEvent), "hook.start")] +[JsonDerivedType(typeof(HumanResponseRecordedEvent), "human_response.recorded")] [JsonDerivedType(typeof(McpAppToolCallCompleteEvent), "mcp_app.tool_call_complete")] [JsonDerivedType(typeof(McpHeadersRefreshCompletedEvent), "mcp.headers_refresh_completed")] [JsonDerivedType(typeof(McpHeadersRefreshRequiredEvent), "mcp.headers_refresh_required")] @@ -167,6 +168,7 @@ namespace GitHub.Copilot; [JsonDerivedType(typeof(ToolExecutionPartialResultEvent), "tool.execution_partial_result")] [JsonDerivedType(typeof(ToolExecutionProgressEvent), "tool.execution_progress")] [JsonDerivedType(typeof(ToolExecutionStartEvent), "tool.execution_start")] +[JsonDerivedType(typeof(ToolShellOutputEvent), "tool.shell_output")] [JsonDerivedType(typeof(ToolUserRequestedEvent), "tool.user_requested")] [JsonDerivedType(typeof(UiEphemeralQueryEvent), "ui.ephemeral_query")] [JsonDerivedType(typeof(UserInputCompletedEvent), "user_input.completed")] @@ -313,6 +315,8 @@ internal sealed partial class SessionEventJsonConverter : JsonConverterStreaming tool execution output for incremental result display. +/// Deprecated merged replacement snapshot of shell output. Use tool.shell_output for append-only, stream-tagged output instead. /// Represents the tool.execution_partial_result event. +[EditorBrowsable(EditorBrowsableState.Never)] +#if NET5_0_OR_GREATER +[Obsolete("This member is deprecated and will be removed in a future version.", DiagnosticId = "GHCP001")] +#else +[Obsolete("This member is deprecated and will be removed in a future version.")] +#endif public sealed partial class ToolExecutionPartialResultEvent : SessionEvent { /// @@ -1660,6 +1672,19 @@ public sealed partial class ToolExecutionPartialResultEvent : SessionEvent public required ToolExecutionPartialResultData Data { get; set; } } +/// Live, append-only shell output. Not persisted or replayed to late subscribers. Text is decoded and redacted per chunk; chunks need not contain complete lines. +/// Represents the tool.shell_output event. +public sealed partial class ToolShellOutputEvent : SessionEvent +{ + /// + [JsonIgnore] + public override string Type => "tool.shell_output"; + + /// The tool.shell_output event payload. + [JsonPropertyName("data")] + public required ToolShellOutputData Data { get; set; } +} + /// Tool execution progress notification with status message. /// Represents the tool.execution_progress event. public sealed partial class ToolExecutionProgressEvent : SessionEvent @@ -2402,6 +2427,19 @@ public sealed partial class ExitPlanModeCompletedEvent : SessionEvent public required ExitPlanModeCompletedData Data { get; set; } } +/// Durable request-correlated evidence for a typed response to a runtime-owned question or plan review. +/// Represents the human_response.recorded event. +public sealed partial class HumanResponseRecordedEvent : SessionEvent +{ + /// + [JsonIgnore] + public override string Type => "human_response.recorded"; + + /// The human_response.recorded event payload. + [JsonPropertyName("data")] + public required HumanResponseRecordedData Data { get; set; } +} + /// Payload of `session.tools_updated` identifying the model whose resolved tools were updated. /// Represents the session.tools_updated event. public sealed partial class SessionToolsUpdatedEvent : SessionEvent @@ -2756,6 +2794,11 @@ public sealed partial class SessionStartData [JsonPropertyName("reasoningEffort")] public string? ReasoningEffort { get; set; } + /// Model that owns effort embedded in an authored model selection. Omitted for independent reasoning-effort overrides and legacy events. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("reasoningEffortModel")] + public string? ReasoningEffortModel { get; set; } + /// Reasoning summary mode used for model calls, if applicable (e.g. "none", "concise", "detailed"). [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("reasoningSummary")] @@ -2836,6 +2879,11 @@ public sealed partial class SessionResumeData [JsonPropertyName("reasoningEffort")] public string? ReasoningEffort { get; set; } + /// Model that owns effort embedded in an authored model selection. Omitted for independent reasoning-effort overrides and legacy events. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("reasoningEffortModel")] + public string? ReasoningEffortModel { get; set; } + /// Reasoning summary mode used for model calls, if applicable (e.g. "none", "concise", "detailed"). [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("reasoningSummary")] @@ -3157,6 +3205,11 @@ public sealed partial class SessionModelChangeData [JsonPropertyName("reasoningEffort")] public string? ReasoningEffort { get; set; } + /// Model that owns effort embedded in an authored model selection. Omitted for independent reasoning-effort overrides and legacy events. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("reasoningEffortModel")] + public string? ReasoningEffortModel { get; set; } + /// Reasoning summary mode after the model change, if applicable. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("reasoningSummary")] @@ -4670,6 +4723,8 @@ public sealed partial class AssistantMessageData [EditorBrowsable(EditorBrowsableState.Never)] #if NET5_0_OR_GREATER [Obsolete("This member is deprecated and will be removed in a future version.", DiagnosticId = "GHCP001")] +#else + [Obsolete("This member is deprecated and will be removed in a future version.")] #endif [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("parentToolCallId")] @@ -4759,6 +4814,8 @@ public sealed partial class AssistantMessageDeltaData [EditorBrowsable(EditorBrowsableState.Never)] #if NET5_0_OR_GREATER [Obsolete("This member is deprecated and will be removed in a future version.", DiagnosticId = "GHCP001")] +#else + [Obsolete("This member is deprecated and will be removed in a future version.")] #endif [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("parentToolCallId")] @@ -4958,6 +5015,8 @@ public sealed partial class AssistantUsageData [EditorBrowsable(EditorBrowsableState.Never)] #if NET5_0_OR_GREATER [Obsolete("This member is deprecated and will be removed in a future version.", DiagnosticId = "GHCP001")] +#else + [Obsolete("This member is deprecated and will be removed in a future version.")] #endif [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("parentToolCallId")] @@ -5478,6 +5537,8 @@ public sealed partial class ToolExecutionStartData [EditorBrowsable(EditorBrowsableState.Never)] #if NET5_0_OR_GREATER [Obsolete("This member is deprecated and will be removed in a future version.", DiagnosticId = "GHCP001")] +#else + [Obsolete("This member is deprecated and will be removed in a future version.")] #endif [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("parentToolCallId")] @@ -5517,10 +5578,16 @@ public sealed partial class ToolExecutionStartData public string? TurnId { get; set; } } -/// Streaming tool execution output for incremental result display. +/// Deprecated merged replacement snapshot of shell output. Use tool.shell_output for append-only, stream-tagged output instead. +[EditorBrowsable(EditorBrowsableState.Never)] +#if NET5_0_OR_GREATER +[Obsolete("This member is deprecated and will be removed in a future version.", DiagnosticId = "GHCP001")] +#else +[Obsolete("This member is deprecated and will be removed in a future version.")] +#endif public sealed partial class ToolExecutionPartialResultData { - /// Incremental output chunk from the running tool. + /// Merged replacement snapshot from the running shell, not an append-only chunk. [JsonPropertyName("partialOutput")] public required string PartialOutput { get; set; } @@ -5529,6 +5596,27 @@ public sealed partial class ToolExecutionPartialResultData public required string ToolCallId { get; set; } } +/// Live, append-only shell output. Not persisted or replayed to late subscribers. Text is decoded and redacted per chunk; chunks need not contain complete lines. +public sealed partial class ToolShellOutputData +{ + /// Zero-based publication sequence across all output streams for this tool call. Not a byte offset or an OS write-order guarantee. + [JsonPropertyName("sequence")] + public required long Sequence { get; set; } + + /// Output source. Omission means stdout. Terminal output has no separate stdout/stderr attribution. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("stream")] + public ToolShellOutputStream? Stream { get; set; } + + /// New output to append, without synthetic shell-result markers or stream-switch separators. + [JsonPropertyName("text")] + public required string Text { get; set; } + + /// Tool call ID that owns this shell output. + [JsonPropertyName("toolCallId")] + public required string ToolCallId { get; set; } +} + /// Tool execution progress notification with status message. public sealed partial class ToolExecutionProgressData { @@ -5555,6 +5643,12 @@ public sealed partial class ToolExecutionCompleteData [JsonPropertyName("error")] public ToolExecutionCompleteError? Error { get; set; } + /// Experimental. File mutations actually committed by a built-in file editing tool, in execution order. Present on successful edits and on partial failures when earlier mutations were committed. Paths are absolute in the session filesystem namespace. + [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("fileEdits")] + public ToolExecutionCompleteFileEdit[]? FileEdits { get; set; } + /// Experimental HydraFusion attribution for this tool completion. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] @@ -5586,6 +5680,8 @@ public sealed partial class ToolExecutionCompleteData [EditorBrowsable(EditorBrowsableState.Never)] #if NET5_0_OR_GREATER [Obsolete("This member is deprecated and will be removed in a future version.", DiagnosticId = "GHCP001")] +#else + [Obsolete("This member is deprecated and will be removed in a future version.")] #endif [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("parentToolCallId")] @@ -7134,6 +7230,27 @@ public sealed partial class ExitPlanModeCompletedData public ExitPlanModeAction? SelectedAction { get; set; } } +/// Durable request-correlated evidence for a typed response to a runtime-owned question or plan review. +public sealed partial class HumanResponseRecordedData +{ + /// Controlled actor provenance established at response ingress. + [JsonPropertyName("actor")] + public required HumanResponseActor Actor { get; set; } + + /// Request ID of the runtime-owned question or plan review. + [JsonPropertyName("requestId")] + public required string RequestId { get; set; } + + /// Typed request and response payload. + [JsonPropertyName("response")] + public required HumanResponseRecordedResponse Response { get; set; } + + /// Tool call ID that opened the request, when present. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("toolCallId")] + public string? ToolCallId { get; set; } +} + /// Payload of `session.tools_updated` identifying the model whose resolved tools were updated. public sealed partial class SessionToolsUpdatedData { @@ -8999,6 +9116,11 @@ public sealed partial class AssistantMessageReasoningBlocks [JsonPropertyName("blocks")] public JsonElement[]? Blocks { get; set; } + /// Anthropic Messages assistant block ordering preserved when the legacy reasoning-only representation cannot reproduce it exactly. Thinking and text blocks remain verbatim; tool-use entries retain identity and a payload fingerprint when later signed reasoning depends on them, and are hydrated from the message's tool requests during replay. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("orderedBlocks")] + public JsonElement[]? OrderedBlocks { get; set; } + /// Model provider that produced these reasoning blocks. [JsonPropertyName("provider")] public required string Provider { get; set; } @@ -9321,6 +9443,20 @@ public sealed partial class ToolExecutionCompleteError public RemediationAction? Remediation { get; set; } } +/// A file mutation that was actually committed by a built-in file editing tool. +/// Nested data type for ToolExecutionCompleteFileEdit. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed partial class ToolExecutionCompleteFileEdit +{ + /// Kind of mutation committed at this path. + [JsonPropertyName("kind")] + public required ToolExecutionCompleteFileEditKind Kind { get; set; } + + /// Absolute path in the session filesystem namespace. + [JsonPropertyName("path")] + public required string Path { get; set; } +} + /// Binary result returned by a tool for the model. /// Nested data type for PersistedBinaryImage. public sealed partial class PersistedBinaryImage @@ -9561,6 +9697,8 @@ public sealed partial class ToolExecutionCompleteContentText : ToolExecutionComp [EditorBrowsable(EditorBrowsableState.Never)] #if NET5_0_OR_GREATER [Obsolete("This member is deprecated and will be removed in a future version.", DiagnosticId = "GHCP001")] +#else +[Obsolete("This member is deprecated and will be removed in a future version.")] #endif public sealed partial class ToolExecutionCompleteContentTerminal : ToolExecutionCompleteContent { @@ -12745,6 +12883,115 @@ public sealed partial class CapabilitiesChangedUI public bool? McpApps { get; set; } } +/// The ask_user variant of . +public sealed partial class HumanResponseRecordedResponseAskUser : HumanResponseRecordedResponse +{ + /// + [JsonIgnore] + public override string ResponseKind => "ask_user"; + + /// Exact answer content accepted from the user. + [JsonPropertyName("content")] + public required IDictionary Content { get; set; } + + /// Exact question displayed to the user. + [JsonPropertyName("message")] + public required string Message { get; set; } + + /// Exact response schema displayed to the user. + [JsonPropertyName("requestedSchema")] + public required ElicitationRequestedSchema RequestedSchema { get; set; } +} + +/// The user_input variant of . +public sealed partial class HumanResponseRecordedResponseUserInput : HumanResponseRecordedResponse +{ + /// + [JsonIgnore] + public override string ResponseKind => "user_input"; + + /// Whether the displayed request allowed a free-form answer. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("allowFreeform")] + public bool? AllowFreeform { get; set; } + + /// Exact selected or free-form answer submitted by the user. + [JsonPropertyName("answer")] + public required string Answer { get; set; } + + /// Exact choices displayed to the user, when the request offered choices. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("choices")] + public string[]? Choices { get; set; } + + /// Exact question displayed to the user. + [JsonPropertyName("question")] + public required string Question { get; set; } + + /// Whether the answer was typed as free-form text rather than selected from the displayed choices. + [JsonPropertyName("wasFreeform")] + public required bool WasFreeform { get; set; } +} + +/// The exit_plan_mode variant of . +public sealed partial class HumanResponseRecordedResponseExitPlanMode : HumanResponseRecordedResponse +{ + /// + [JsonIgnore] + public override string ResponseKind => "exit_plan_mode"; + + /// Actions offered by the plan review UI. + [JsonPropertyName("actions")] + public required ExitPlanModeAction[] Actions { get; set; } + + /// Whether the user approved the reviewed plan. + [JsonPropertyName("approved")] + public required bool Approved { get; set; } + + /// Whether the selected response requested edit auto-approval. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("autoApproveEdits")] + public bool? AutoApproveEdits { get; set; } + + /// Exact feedback submitted with the plan decision, when present. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("feedback")] + public string? Feedback { get; set; } + + /// Exact full plan content available from the review UI. + [JsonPropertyName("planContent")] + public required string PlanContent { get; set; } + + /// Action the plan review UI recommended. + [JsonPropertyName("recommendedAction")] + public required ExitPlanModeAction RecommendedAction { get; set; } + + /// Action selected by the user, when applicable. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("selectedAction")] + public ExitPlanModeAction? SelectedAction { get; set; } + + /// Exact plan summary displayed to the user. + [JsonPropertyName("summary")] + public required string Summary { get; set; } +} + +/// Exact runtime-owned question or reviewed plan paired with the typed response that settled it. +/// Polymorphic base type discriminated by responseKind. +[JsonPolymorphic( + TypeDiscriminatorPropertyName = "responseKind", + UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FallBackToBaseType)] +[JsonDerivedType(typeof(HumanResponseRecordedResponseAskUser), "ask_user")] +[JsonDerivedType(typeof(HumanResponseRecordedResponseUserInput), "user_input")] +[JsonDerivedType(typeof(HumanResponseRecordedResponseExitPlanMode), "exit_plan_mode")] +public partial class HumanResponseRecordedResponse +{ + /// The type discriminator. + [JsonPropertyName("responseKind")] + public virtual string ResponseKind { get; set; } = string.Empty; +} + + /// A single resolved skill in `session.skills_loaded`, including source, invocability, enabled state, path, and argument hint. /// Nested data type for SkillsLoadedSkill. public sealed partial class SkillsLoadedSkill @@ -17222,6 +17469,135 @@ public override void Write(Utf8JsonWriter writer, ToolExecutionStartToolDescript } } +/// Shell output source. Terminal output has no separate stdout/stderr attribution. +[JsonConverter(typeof(Converter))] +[DebuggerDisplay("{Value,nq}")] +public readonly struct ToolShellOutputStream : IEquatable +{ + private readonly string? _value; + + /// Initializes a new instance of the struct. + /// The value to associate with this . + [JsonConstructor] + public ToolShellOutputStream(string value) + { + ArgumentException.ThrowIfNullOrWhiteSpace(value); + _value = value; + } + + /// Gets the value associated with this . + public string Value => _value ?? string.Empty; + + /// Output from the shell command's standard output stream. This is the default when stream is omitted. + public static ToolShellOutputStream Stdout { get; } = new("stdout"); + + /// Output from the shell command's standard error stream. + public static ToolShellOutputStream Stderr { get; } = new("stderr"); + + /// Inherently merged output that cannot be attributed separately to stdout or stderr. + public static ToolShellOutputStream Terminal { get; } = new("terminal"); + + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(ToolShellOutputStream left, ToolShellOutputStream right) => left.Equals(right); + + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(ToolShellOutputStream left, ToolShellOutputStream right) => !(left == right); + + /// + public override bool Equals(object? obj) => obj is ToolShellOutputStream other && Equals(other); + + /// + public bool Equals(ToolShellOutputStream other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + + /// + public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); + + /// + public override string ToString() => Value; + + /// Provides a for serializing instances. + [EditorBrowsable(EditorBrowsableState.Never)] + public sealed class Converter : JsonConverter + { + /// + public override ToolShellOutputStream Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); + } + + /// + public override void Write(Utf8JsonWriter writer, ToolShellOutputStream value, JsonSerializerOptions options) + { + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(ToolShellOutputStream)); + } + } +} + +/// Kind of file mutation committed by a built-in editing tool. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +[JsonConverter(typeof(Converter))] +[DebuggerDisplay("{Value,nq}")] +public readonly struct ToolExecutionCompleteFileEditKind : IEquatable +{ + private readonly string? _value; + + /// Initializes a new instance of the struct. + /// The value to associate with this . + [JsonConstructor] + public ToolExecutionCompleteFileEditKind(string value) + { + ArgumentException.ThrowIfNullOrWhiteSpace(value); + _value = value; + } + + /// Gets the value associated with this . + public string Value => _value ?? string.Empty; + + /// A file was created. + public static ToolExecutionCompleteFileEditKind Create { get; } = new("create"); + + /// A file was written by an edit operation. + public static ToolExecutionCompleteFileEditKind Edit { get; } = new("edit"); + + /// A file was deleted. + public static ToolExecutionCompleteFileEditKind Delete { get; } = new("delete"); + + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(ToolExecutionCompleteFileEditKind left, ToolExecutionCompleteFileEditKind right) => left.Equals(right); + + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(ToolExecutionCompleteFileEditKind left, ToolExecutionCompleteFileEditKind right) => !(left == right); + + /// + public override bool Equals(object? obj) => obj is ToolExecutionCompleteFileEditKind other && Equals(other); + + /// + public bool Equals(ToolExecutionCompleteFileEditKind other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + + /// + public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); + + /// + public override string ToString() => Value; + + /// Provides a for serializing instances. + [EditorBrowsable(EditorBrowsableState.Never)] + public sealed class Converter : JsonConverter + { + /// + public override ToolExecutionCompleteFileEditKind Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); + } + + /// + public override void Write(Utf8JsonWriter writer, ToolExecutionCompleteFileEditKind value, JsonSerializerOptions options) + { + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(ToolExecutionCompleteFileEditKind)); + } + } +} + /// Binary result type discriminator. Use "image" for images and "resource" for other binary data. [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] @@ -20787,6 +21163,70 @@ public override void Write(Utf8JsonWriter writer, ExitPlanModeAction value, Json } } +/// Controlled provenance for a typed runtime response. Only `human_response`, minted by a trusted direct-interaction ingress, is human authorization evidence. +[JsonConverter(typeof(Converter))] +[DebuggerDisplay("{Value,nq}")] +public readonly struct HumanResponseActor : IEquatable +{ + private readonly string? _value; + + /// Initializes a new instance of the struct. + /// The value to associate with this . + [JsonConstructor] + public HumanResponseActor(string value) + { + ArgumentException.ThrowIfNullOrWhiteSpace(value); + _value = value; + } + + /// Gets the value associated with this . + public string Value => _value ?? string.Empty; + + /// A built-in trusted client submitted the response after direct human interaction. + public static HumanResponseActor HumanResponse { get; } = new("human_response"); + + /// A host or SDK automation submitted the response without direct human interaction. + public static HumanResponseActor HostAutomation { get; } = new("host_automation"); + + /// The response came through a legacy or otherwise unattributed ingress. + public static HumanResponseActor Unknown { get; } = new("unknown"); + + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(HumanResponseActor left, HumanResponseActor right) => left.Equals(right); + + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(HumanResponseActor left, HumanResponseActor right) => !(left == right); + + /// + public override bool Equals(object? obj) => obj is HumanResponseActor other && Equals(other); + + /// + public bool Equals(HumanResponseActor other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + + /// + public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); + + /// + public override string ToString() => Value; + + /// Provides a for serializing instances. + [EditorBrowsable(EditorBrowsableState.Never)] + public sealed class Converter : JsonConverter + { + /// + public override HumanResponseActor Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); + } + + /// + public override void Write(Utf8JsonWriter writer, HumanResponseActor value, JsonSerializerOptions options) + { + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(HumanResponseActor)); + } + } +} + /// Terminal status a workflow run committed. A settled run is never `pending` or `running`, so those two members of the run-status domain are deliberately absent. [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] @@ -21349,6 +21789,12 @@ public override void Write(Utf8JsonWriter writer, ExtensionsLoadedExtensionStatu [JsonSerializable(typeof(HookProgressEvent))] [JsonSerializable(typeof(HookStartData))] [JsonSerializable(typeof(HookStartEvent))] +[JsonSerializable(typeof(HumanResponseRecordedData))] +[JsonSerializable(typeof(HumanResponseRecordedEvent))] +[JsonSerializable(typeof(HumanResponseRecordedResponse))] +[JsonSerializable(typeof(HumanResponseRecordedResponseAskUser))] +[JsonSerializable(typeof(HumanResponseRecordedResponseExitPlanMode))] +[JsonSerializable(typeof(HumanResponseRecordedResponseUserInput))] [JsonSerializable(typeof(McpAppToolCallCompleteData))] [JsonSerializable(typeof(McpAppToolCallCompleteError))] [JsonSerializable(typeof(McpAppToolCallCompleteEvent))] @@ -21664,6 +22110,7 @@ public override void Write(Utf8JsonWriter writer, ExtensionsLoadedExtensionStatu [JsonSerializable(typeof(ToolExecutionCompleteData))] [JsonSerializable(typeof(ToolExecutionCompleteError))] [JsonSerializable(typeof(ToolExecutionCompleteEvent))] +[JsonSerializable(typeof(ToolExecutionCompleteFileEdit))] [JsonSerializable(typeof(ToolExecutionCompleteResult))] [JsonSerializable(typeof(ToolExecutionCompleteShellExecution))] [JsonSerializable(typeof(ToolExecutionCompleteToolDescription))] @@ -21690,6 +22137,8 @@ public override void Write(Utf8JsonWriter writer, ExtensionsLoadedExtensionStatu [JsonSerializable(typeof(ToolExecutionStartToolDescriptionMetaUI))] [JsonSerializable(typeof(ToolSearchActivatedData))] [JsonSerializable(typeof(ToolSearchActivatedEvent))] +[JsonSerializable(typeof(ToolShellOutputData))] +[JsonSerializable(typeof(ToolShellOutputEvent))] [JsonSerializable(typeof(ToolUserRequestedData))] [JsonSerializable(typeof(ToolUserRequestedEvent))] [JsonSerializable(typeof(UiEphemeralQueryData))] diff --git a/dotnet/src/Session.cs b/dotnet/src/Session.cs index 01a5e553f8..f0cdde8999 100644 --- a/dotnet/src/Session.cs +++ b/dotnet/src/Session.cs @@ -1200,6 +1200,17 @@ await Rpc.Tools.HandlePendingToolCallAsync( static string GetSingleParameterName(AIFunction tool) { + if (tool.Name == "apply_patch" && + tool.AdditionalProperties.TryGetValue(CopilotTool.OverridesBuiltInToolKey, out var isOverride) && + isOverride is true && + tool.JsonSchema.TryGetProperty("type", out var schemaType) && + schemaType.ValueKind == JsonValueKind.String && + schemaType.GetString() == "string") + { + // AIFunction uses named arguments even when the override declares a scalar schema. + return "input"; + } + if (tool.JsonSchema.TryGetProperty("properties", out var properties) && properties.ValueKind == JsonValueKind.Object) { @@ -2003,6 +2014,16 @@ internal void RegisterHooks(SessionHooks hooks) JsonSerializer.Deserialize(input.GetRawText(), SessionJsonContext.Default.AgentStopHookInput)!, invocation) : null, + "subagentStart" => hooks.OnSubagentStart != null + ? await hooks.OnSubagentStart( + JsonSerializer.Deserialize(input.GetRawText(), SessionJsonContext.Default.SubagentStartHookInput)!, + invocation) + : null, + "subagentStop" => hooks.OnSubagentStop != null + ? await hooks.OnSubagentStop( + JsonSerializer.Deserialize(input.GetRawText(), SessionJsonContext.Default.SubagentStopHookInput)!, + invocation) + : null, _ => null }; } @@ -2509,6 +2530,10 @@ internal void ThrowIfDisposed() [JsonSerializable(typeof(SessionEndHookOutput))] [JsonSerializable(typeof(SessionStartHookInput))] [JsonSerializable(typeof(SessionStartHookOutput))] + [JsonSerializable(typeof(SubagentStartHookInput))] + [JsonSerializable(typeof(SubagentStartHookOutput))] + [JsonSerializable(typeof(SubagentStopHookInput))] + [JsonSerializable(typeof(SubagentStopHookOutput))] [JsonSerializable(typeof(SystemMessageTransformRpcResponse))] [JsonSerializable(typeof(SystemMessageTransformSection))] [JsonSerializable(typeof(Attachment))] diff --git a/dotnet/src/SessionFsProvider.cs b/dotnet/src/SessionFsProvider.cs index a353c93ad0..9aaf25f4b7 100644 --- a/dotnet/src/SessionFsProvider.cs +++ b/dotnet/src/SessionFsProvider.cs @@ -73,6 +73,22 @@ public interface ISessionFsSqliteProvider Task ExistsAsync(CancellationToken cancellationToken); } +/// Optional interface for providers that support exact binary file reads and writes. +public interface ISessionFsBinaryProvider +{ + /// Reads exact bytes of a file in the session filesystem. + /// Path to read. + /// Cancellation token. + Task ReadFileBytesAsync(string path, CancellationToken cancellationToken); + + /// Writes exact bytes of a file in the session filesystem. + /// Path to write. + /// Exact file bytes. + /// Optional POSIX-style permission mode. + /// Cancellation token. + Task WriteFileBytesAsync(string path, ReadOnlyMemory content, int? mode, CancellationToken cancellationToken); +} + /// /// Optional capability for session filesystem providers that support atomic SQLite transactions. /// @@ -119,73 +135,92 @@ public SessionFsSqliteTransactionException( public SessionFsSqliteTransactionErrorClass ErrorClass { get; } } +/// Thrown from a provider's WriteFileAsync when the failed write changed its target. +public sealed class SessionFsWriteException : Exception +{ + /// Initializes a failed write that changed its target. + /// Human-readable failure description. + /// Optional underlying exception. + public SessionFsWriteException(string message, Exception? innerException = null) + : base(message, innerException) + { + } +} + /// /// Base class for session filesystem providers. Subclasses override the /// virtual methods and use normal C# patterns (return values, throw exceptions). /// The base class catches exceptions and converts them to -/// results expected by the runtime. +/// results expected by the runtime. Existence checks instead return false if the provider throws. /// To add SQLite support, also implement . /// +/// +/// Paths use the configured and are passed to the provider unchanged. +/// Absolute paths refer to the provider's filesystem namespace, not necessarily the runtime host's filesystem. +/// public abstract class SessionFsProvider : ISessionFsHandler { + private const int MaxBinaryBytes = (64 * 1024 * 1024 - 1024) / 4 * 3; + private const int MaxBinaryContentLength = (MaxBinaryBytes + 2) / 3 * 4; + /// Reads the full content of a file. Throw if the file does not exist. - /// SessionFs-relative path. + /// Absolute or relative path in the provider's filesystem namespace. /// Cancellation token. /// The file content as a UTF-8 string. protected abstract Task ReadFileAsync(string path, CancellationToken cancellationToken); - /// Writes content to a file, creating it (and parent directories) if needed. - /// SessionFs-relative path. + /// Writes content to a file, creating it (and parent directories) if needed. Throw if a failed write changed the target. + /// Absolute or relative path in the provider's filesystem namespace. /// Content to write. /// Optional POSIX-style permission mode. Null means use OS default. /// Cancellation token. protected abstract Task WriteFileAsync(string path, string content, int? mode, CancellationToken cancellationToken); /// Appends content to a file, creating it (and parent directories) if needed. - /// SessionFs-relative path. + /// Absolute or relative path in the provider's filesystem namespace. /// Content to append. /// Optional POSIX-style permission mode. Null means use OS default. /// Cancellation token. protected abstract Task AppendFileAsync(string path, string content, int? mode, CancellationToken cancellationToken); /// Checks whether a path exists. - /// SessionFs-relative path. + /// Absolute or relative path in the provider's filesystem namespace. /// Cancellation token. /// true if the path exists, false otherwise. protected abstract Task ExistsAsync(string path, CancellationToken cancellationToken); /// Gets metadata about a file or directory. Throw if the path does not exist. - /// SessionFs-relative path. + /// Absolute or relative path in the provider's filesystem namespace. /// Cancellation token. protected abstract Task StatAsync(string path, CancellationToken cancellationToken); - /// Creates a directory (and optionally parents). Does not fail if it already exists. - /// SessionFs-relative path. + /// Creates a directory. If is true, creates parent directories as needed. + /// Absolute or relative path in the provider's filesystem namespace. /// Whether to create parent directories. /// Optional POSIX-style permission mode (e.g., 0x1FF for 0777). Null means use OS default. /// Cancellation token. protected abstract Task MakeDirectoryAsync(string path, bool recursive, int? mode, CancellationToken cancellationToken); /// Lists entry names in a directory. Throw if the directory does not exist. - /// SessionFs-relative path. + /// Absolute or relative path in the provider's filesystem namespace. /// Cancellation token. protected abstract Task> ReadDirectoryAsync(string path, CancellationToken cancellationToken); /// Lists entries with type info in a directory. Throw if the directory does not exist. - /// SessionFs-relative path. + /// Absolute or relative path in the provider's filesystem namespace. /// Cancellation token. protected abstract Task> ReadDirectoryWithTypesAsync(string path, CancellationToken cancellationToken); /// Removes a file or directory. Throw if the path does not exist (unless is true). - /// SessionFs-relative path. + /// Absolute or relative path in the provider's filesystem namespace. /// Whether to remove directory contents recursively. /// If true, do not throw when the path does not exist. /// Cancellation token. protected abstract Task RemoveAsync(string path, bool recursive, bool force, CancellationToken cancellationToken); /// Renames/moves a file or directory. - /// Source path. - /// Destination path. + /// Absolute or relative source path in the provider's filesystem namespace. + /// Absolute or relative destination path in the provider's filesystem namespace. /// Cancellation token. protected abstract Task RenameAsync(string src, string dest, CancellationToken cancellationToken); @@ -206,6 +241,37 @@ async Task ISessionFsHandler.ReadFileAsync(SessionFsRea } } + async Task ISessionFsHandler.ReadFileBytesAsync(SessionFsReadFileBytesRequest request, CancellationToken cancellationToken) + { + ArgumentNullException.ThrowIfNull(request); + + try + { + if (this is not ISessionFsBinaryProvider provider) + { + throw new NotSupportedException("Binary reads are not supported by this provider."); + } + var bytes = await provider.ReadFileBytesAsync(request.Path, cancellationToken).ConfigureAwait(false); + if (bytes.Length > MaxBinaryBytes) + { + return new SessionFsReadFileBytesResult + { + Content = string.Empty, + Error = new SessionFsError + { + Code = SessionFsErrorCode.UNKNOWN, + Message = "sessionFs.readFileBytes content exceeds the binary read limit" + } + }; + } + return new SessionFsReadFileBytesResult { Content = Convert.ToBase64String(bytes) }; + } + catch (Exception ex) when (ex is not OperationCanceledException) + { + return new SessionFsReadFileBytesResult { Content = string.Empty, Error = ToSessionFsError(ex) }; + } + } + async Task ISessionFsHandler.WriteFileAsync(SessionFsWriteFileRequest request, CancellationToken cancellationToken) { ArgumentNullException.ThrowIfNull(request); @@ -216,6 +282,39 @@ async Task ISessionFsHandler.ReadFileAsync(SessionFsRea return null; } catch (Exception ex) + { + var error = ToSessionFsError(ex); + if (ex is SessionFsWriteException) + { + error.WriteChanged = true; + } + return error; + } + } + + async Task ISessionFsHandler.WriteFileBytesAsync(SessionFsWriteFileBytesRequest request, CancellationToken cancellationToken) + { + ArgumentNullException.ThrowIfNull(request); + + try + { + if (this is not ISessionFsBinaryProvider provider) + { + throw new NotSupportedException("Binary writes are not supported by this provider."); + } + if (request.Content.Length > MaxBinaryContentLength) + { + return new SessionFsError { Code = SessionFsErrorCode.UNKNOWN, Message = "sessionFs.writeFileBytes content exceeds the binary write limit" }; + } + var content = Convert.FromBase64String(request.Content); + if (Convert.ToBase64String(content) != request.Content) + { + return new SessionFsError { Code = SessionFsErrorCode.UNKNOWN, Message = "invalid sessionFs.writeFileBytes base64 content" }; + } + await provider.WriteFileBytesAsync(request.Path, content, (int?)request.Mode, cancellationToken).ConfigureAwait(false); + return null; + } + catch (Exception ex) when (ex is not OperationCanceledException) { return ToSessionFsError(ex); } diff --git a/dotnet/src/Types.cs b/dotnet/src/Types.cs index ee0677788e..7ed04c279b 100644 --- a/dotnet/src/Types.cs +++ b/dotnet/src/Types.cs @@ -2052,6 +2052,165 @@ public sealed class AgentStopHookOutput public string? Reason { get; set; } } +/// +/// Input for a subagent-start hook, before the sub-agent's first turn. +/// +public sealed class SubagentStartHookInput +{ + /// + /// The runtime session ID of the parent session. + /// + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; + + /// + /// Unix timestamp in milliseconds when the hook was triggered. + /// + [JsonPropertyName("timestamp")] + [JsonConverter(typeof(UnixMillisecondsDateTimeOffsetConverter))] + public DateTimeOffset Timestamp { get; set; } + + /// + /// Current working directory of the parent session. + /// + [JsonPropertyName("cwd")] + public string WorkingDirectory { get; set; } = string.Empty; + + /// + /// Path to the parent session's on-disk transcript, or empty if unavailable. + /// + [JsonPropertyName("transcriptPath")] + public string TranscriptPath { get; set; } = string.Empty; + + /// + /// Name of the sub-agent being started. + /// + [JsonPropertyName("agentName")] + public string AgentName { get; set; } = string.Empty; + + /// + /// Display name of the sub-agent, when available. + /// + [JsonPropertyName("agentDisplayName")] + public string? AgentDisplayName { get; set; } + + /// + /// Description of the sub-agent, when available. + /// + [JsonPropertyName("agentDescription")] + public string? AgentDescription { get; set; } +} + +/// +/// Output for a subagent-start hook. +/// +public sealed class SubagentStartHookOutput +{ + /// + /// Additional context prepended to the sub-agent's initial prompt. + /// + [JsonPropertyName("additionalContext")] + public string? AdditionalContext { get; set; } +} + +/// +/// Input for a subagent-stop hook, after a sub-agent turn completes. +/// +public sealed class SubagentStopHookInput +{ + /// + /// The runtime session ID of the parent session. + /// + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; + + /// + /// Unix timestamp in milliseconds when the hook was triggered. + /// + [JsonPropertyName("timestamp")] + [JsonConverter(typeof(UnixMillisecondsDateTimeOffsetConverter))] + public DateTimeOffset Timestamp { get; set; } + + /// + /// Current working directory of the parent session. + /// + [JsonPropertyName("cwd")] + public string WorkingDirectory { get; set; } = string.Empty; + + /// + /// Path to the parent session's on-disk transcript, or empty if unavailable. + /// + [JsonPropertyName("transcriptPath")] + public string TranscriptPath { get; set; } = string.Empty; + + /// + /// Name of the sub-agent that stopped. + /// + [JsonPropertyName("agentName")] + public string AgentName { get; set; } = string.Empty; + + /// + /// Display name of the sub-agent, when available. + /// + [JsonPropertyName("agentDisplayName")] + public string? AgentDisplayName { get; set; } + + /// + /// Description of the sub-agent, when available. + /// + [JsonPropertyName("agentDescription")] + public string? AgentDescription { get; set; } + + /// + /// Identifier of the sub-agent, when available. + /// + [JsonPropertyName("agentId")] + public string? AgentId { get; set; } + + /// + /// Type of the sub-agent that stopped. + /// + [JsonPropertyName("agentType")] + public string AgentType { get; set; } = string.Empty; + + /// + /// Reason the sub-agent stopped, such as "end_turn". + /// + [JsonPropertyName("stopReason")] + public string StopReason { get; set; } = string.Empty; + + /// + /// Last assistant response from the sub-agent. + /// + [JsonPropertyName("response")] + public string Response { get; set; } = string.Empty; +} + +/// +/// Output for a subagent-stop hook. +/// +public sealed class SubagentStopHookOutput +{ + /// + /// Set to "block" to request another sub-agent turn, or "allow" to accept it. + /// Other values fail the sub-agent instead of silently allowing its stop. + /// + [JsonPropertyName("decision")] + public string? Decision { get; set; } + + /// + /// Nonempty instruction required when the stop is blocked; invalid without Decision = "block". + /// + [JsonPropertyName("reason")] + public string? Reason { get; set; } + + /// + /// Replacement for the sub-agent's final response when the stop is not blocked. + /// + [JsonPropertyName("modifiedResponse")] + public string? ModifiedResponse { get; set; } +} + /// /// Hook handlers configuration for a session. /// @@ -2108,6 +2267,16 @@ public sealed class SessionHooks /// Handler called when the top-level agent reaches a natural stop. /// public Func>? OnAgentStop { get; set; } + + /// + /// Handler called before a sub-agent's first turn. + /// + public Func>? OnSubagentStart { get; set; } + + /// + /// Handler called after a sub-agent turn completes. + /// + public Func>? OnSubagentStop { get; set; } } /// @@ -2284,9 +2453,9 @@ public sealed class SectionOverride public static SystemMessageSection ToolInstructions { get; } = new("tool_instructions"); /// Repository and organization custom instructions. public static SystemMessageSection CustomInstructions { get; } = new("custom_instructions"); - /// Runtime-provided context and instructions (e.g. system notifications, memories, workspace context, mode-specific instructions, content-exclusion policy). + /// Runtime-provided system-prompt context and instructions, such as system notifications, memories, workspace context, and content-exclusion policy. Mode-specific instructions can travel in transition messages instead. public static SystemMessageSection RuntimeInstructions { get; } = new("runtime_instructions"); - /// End-of-prompt instructions: parallel tool calling, persistence, task completion. + /// End-of-prompt instructions: parallel tool calling, persistence, task completion, and configured subagent-model guidance when the task tool is available. public static SystemMessageSection LastInstructions { get; } = new("last_instructions"); /// Gets the underlying string value of this . diff --git a/dotnet/src/build/GitHub.Copilot.SDK.targets b/dotnet/src/build/GitHub.Copilot.SDK.targets index 0c9bfbaf5a..1775fb1159 100644 --- a/dotnet/src/build/GitHub.Copilot.SDK.targets +++ b/dotnet/src/build/GitHub.Copilot.SDK.targets @@ -196,7 +196,7 @@ <_CopilotRuntimeRootAsset Include="$(_CopilotCacheDir)\**\*" - Exclude="$(_CopilotCacheDir)\.copilot-runtime-complete;$(_CopilotCacheDir)\app.js;$(_CopilotCacheDir)\assets\**\*;$(_CopilotCacheDir)\changelog.json;$(_CopilotCacheDir)\copilot;$(_CopilotCacheDir)\copilot.exe;$(_CopilotCacheDir)\copilot.tgz;$(_CopilotCacheDir)\foundry-local-sdk\**\*;$(_CopilotCacheDir)\index.js;$(_CopilotCacheDir)\LICENSE.md;$(_CopilotCacheDir)\napi-oop-runtime\**\*;$(_CopilotCacheDir)\npm-loader.js;$(_CopilotCacheDir)\package.json;$(_CopilotCacheDir)\prebuilds\**\*;$(_CopilotCacheDir)\pvrecorder\**\*;$(_CopilotCacheDir)\queries\**\*;$(_CopilotCacheDir)\README.md;$(_CopilotCacheDir)\sea-loader.js;$(_CopilotCacheDir)\SHA256SUMS.txt;$(_CopilotCacheDir)\tree-sitter*.wasm;$(_CopilotCacheDir)\voice-*.js;$(_CopilotCacheDir)\webview\**\*" /> + Exclude="$(_CopilotCacheDir)\.copilot-runtime-complete;$(_CopilotCacheDir)\app.js;$(_CopilotCacheDir)\assets\**\*;$(_CopilotCacheDir)\changelog.json;$(_CopilotCacheDir)\cli-main.js;$(_CopilotCacheDir)\copilot;$(_CopilotCacheDir)\copilot.exe;$(_CopilotCacheDir)\copilot.tgz;$(_CopilotCacheDir)\foundry-local-sdk\**\*;$(_CopilotCacheDir)\index.js;$(_CopilotCacheDir)\LICENSE.md;$(_CopilotCacheDir)\napi-oop-runtime\**\*;$(_CopilotCacheDir)\npm-loader.js;$(_CopilotCacheDir)\package.json;$(_CopilotCacheDir)\prebuilds\**\*;$(_CopilotCacheDir)\pvrecorder\**\*;$(_CopilotCacheDir)\queries\**\*;$(_CopilotCacheDir)\README.md;$(_CopilotCacheDir)\sea-loader.js;$(_CopilotCacheDir)\SHA256SUMS.txt;$(_CopilotCacheDir)\tree-sitter*.wasm;$(_CopilotCacheDir)\voice-*.js;$(_CopilotCacheDir)\webview\**\*" /> <_CopilotRuntimePrebuildAsset Include="$(_CopilotCacheDir)\prebuilds\$(_CopilotPlatform)\**\*" Exclude="$(_CopilotCacheDir)\prebuilds\$(_CopilotPlatform)\cli-native.node;$(_CopilotCacheDir)\prebuilds\$(_CopilotPlatform)\mediaremote-adapter\**\*;$(_CopilotCacheDir)\prebuilds\$(_CopilotPlatform)\copilot-runtime-bin*" /> @@ -238,7 +238,7 @@ Condition="'$(CopilotCliBinaryPath)' != ''" /> <_CopilotRuntimeRootAsset Include="$(_CopilotCacheDir)\**\*" - Exclude="$(_CopilotCacheDir)\.copilot-runtime-complete;$(_CopilotCacheDir)\app.js;$(_CopilotCacheDir)\assets\**\*;$(_CopilotCacheDir)\changelog.json;$(_CopilotCacheDir)\copilot;$(_CopilotCacheDir)\copilot.exe;$(_CopilotCacheDir)\copilot.tgz;$(_CopilotCacheDir)\foundry-local-sdk\**\*;$(_CopilotCacheDir)\index.js;$(_CopilotCacheDir)\LICENSE.md;$(_CopilotCacheDir)\napi-oop-runtime\**\*;$(_CopilotCacheDir)\npm-loader.js;$(_CopilotCacheDir)\package.json;$(_CopilotCacheDir)\prebuilds\**\*;$(_CopilotCacheDir)\pvrecorder\**\*;$(_CopilotCacheDir)\queries\**\*;$(_CopilotCacheDir)\README.md;$(_CopilotCacheDir)\sea-loader.js;$(_CopilotCacheDir)\SHA256SUMS.txt;$(_CopilotCacheDir)\tree-sitter*.wasm;$(_CopilotCacheDir)\voice-*.js;$(_CopilotCacheDir)\webview\**\*" + Exclude="$(_CopilotCacheDir)\.copilot-runtime-complete;$(_CopilotCacheDir)\app.js;$(_CopilotCacheDir)\assets\**\*;$(_CopilotCacheDir)\changelog.json;$(_CopilotCacheDir)\cli-main.js;$(_CopilotCacheDir)\copilot;$(_CopilotCacheDir)\copilot.exe;$(_CopilotCacheDir)\copilot.tgz;$(_CopilotCacheDir)\foundry-local-sdk\**\*;$(_CopilotCacheDir)\index.js;$(_CopilotCacheDir)\LICENSE.md;$(_CopilotCacheDir)\napi-oop-runtime\**\*;$(_CopilotCacheDir)\npm-loader.js;$(_CopilotCacheDir)\package.json;$(_CopilotCacheDir)\prebuilds\**\*;$(_CopilotCacheDir)\pvrecorder\**\*;$(_CopilotCacheDir)\queries\**\*;$(_CopilotCacheDir)\README.md;$(_CopilotCacheDir)\sea-loader.js;$(_CopilotCacheDir)\SHA256SUMS.txt;$(_CopilotCacheDir)\tree-sitter*.wasm;$(_CopilotCacheDir)\voice-*.js;$(_CopilotCacheDir)\webview\**\*" Condition="Exists('$(_CopilotRuntimeWrapperPath)') And Exists('$(_CopilotRuntimeNodePath)')" /> <_CopilotRuntimePrebuildAsset Include="$(_CopilotCacheDir)\prebuilds\$(_CopilotPlatform)\**\*" Exclude="$(_CopilotCacheDir)\prebuilds\$(_CopilotPlatform)\cli-native.node;$(_CopilotCacheDir)\prebuilds\$(_CopilotPlatform)\mediaremote-adapter\**\*;$(_CopilotCacheDir)\prebuilds\$(_CopilotPlatform)\copilot-runtime-bin*" diff --git a/dotnet/test/E2E/CompactionE2ETests.cs b/dotnet/test/E2E/CompactionE2ETests.cs index 63d4675358..3b85b64a57 100644 --- a/dotnet/test/E2E/CompactionE2ETests.cs +++ b/dotnet/test/E2E/CompactionE2ETests.cs @@ -52,7 +52,20 @@ await session.SendAndWaitAsync(new MessageOptions Assert.True(startEvent.Data.ConversationTokens.GetValueOrDefault() > 0, "Expected compaction to report conversation tokens at start"); Assert.True(completeEvent.Data.Success, "Expected compaction to succeed"); Assert.NotNull(completeEvent.Data.CompactionTokensUsed); - Assert.True(completeEvent.Data.CompactionTokensUsed!.InputTokens.GetValueOrDefault() > 0, "Expected compaction call to consume input tokens"); + // Replay has no prompt usage; recording must preserve the provider's value or absence. + if (Environment.GetEnvironmentVariable("GITHUB_ACTIONS") == "true" + || E2ETestBackendConfiguration.Current != E2ETestBackend.Capi) + { + Assert.Equal(0, completeEvent.Data.CompactionTokensUsed!.InputTokens.GetValueOrDefault()); + } + else + { + var exchanges = await Ctx.GetExchangesAsync(); + var compaction = Assert.Single(exchanges, exchange => + exchange.CompactionUsage is not null + && exchange.CompactionUsage.Summary == completeEvent.Data.SummaryContent); + Assert.Equal(compaction.CompactionUsage!.InputTokens, completeEvent.Data.CompactionTokensUsed!.InputTokens); + } Assert.Contains("", completeEvent.Data.SummaryContent ?? string.Empty, StringComparison.OrdinalIgnoreCase); Assert.Contains("", completeEvent.Data.SummaryContent ?? string.Empty, StringComparison.OrdinalIgnoreCase); Assert.Contains("", completeEvent.Data.SummaryContent ?? string.Empty, StringComparison.OrdinalIgnoreCase); diff --git a/dotnet/test/E2E/HookLifecycleAndOutputE2ETests.cs b/dotnet/test/E2E/HookLifecycleAndOutputE2ETests.cs index 18ad960967..39fc5cf3af 100644 --- a/dotnet/test/E2E/HookLifecycleAndOutputE2ETests.cs +++ b/dotnet/test/E2E/HookLifecycleAndOutputE2ETests.cs @@ -10,13 +10,14 @@ namespace GitHub.Copilot.Test.E2E; /// -/// E2E coverage for every handler exposed on : +/// E2E coverage for session and tool handlers exposed on : /// OnPreToolUse, OnPostToolUse, OnPostToolUseFailure, OnUserPromptSubmitted, /// OnUserPromptTransformed, OnSessionStart, OnSessionEnd, OnErrorOccurred, /// OnAgentStop. Output-shape behavior (modifiedPrompt / modifiedTransformedPrompt / /// additionalContext / errorHandling / modifiedArgs / /// modifiedResult / sessionSummary) is asserted alongside hook invocation. If a -/// new handler is added to SessionHooks, add a corresponding test here. +/// new session or tool handler is added to SessionHooks, add a corresponding +/// test here. Sub-agent lifecycle hooks are covered by . /// public class HookLifecycleAndOutputE2ETests(E2ETestFixture fixture, ITestOutputHelper output) : E2ETestBase(fixture, "hooks_extended", output) diff --git a/dotnet/test/E2E/PermissionE2ETests.cs b/dotnet/test/E2E/PermissionE2ETests.cs index 3e01f2132b..78e792c2c2 100644 --- a/dotnet/test/E2E/PermissionE2ETests.cs +++ b/dotnet/test/E2E/PermissionE2ETests.cs @@ -625,6 +625,7 @@ private static string DescribeEvent(SessionEvent evt) _ => evt.Type, }; +#pragma warning disable CS0618 // Compatibility coverage still reads the legacy terminal variant. private static bool ToolCompleteContains(ToolExecutionCompleteEvent evt, string expected) => evt.Data.Result?.Content.Contains(expected, StringComparison.OrdinalIgnoreCase) == true || evt.Data.Result?.DetailedContent?.Contains(expected, StringComparison.OrdinalIgnoreCase) == true || @@ -634,4 +635,5 @@ private static bool ToolCompleteContains(ToolExecutionCompleteEvent evt, string ToolExecutionCompleteContentTerminal terminal => terminal.Text.Contains(expected, StringComparison.OrdinalIgnoreCase), _ => false, }) == true; +#pragma warning restore CS0618 } diff --git a/dotnet/test/E2E/RpcShellAndFleetE2ETests.cs b/dotnet/test/E2E/RpcShellAndFleetE2ETests.cs index 728b8aaf7c..fd023bcd11 100644 --- a/dotnet/test/E2E/RpcShellAndFleetE2ETests.cs +++ b/dotnet/test/E2E/RpcShellAndFleetE2ETests.cs @@ -70,7 +70,7 @@ private static string CreateWriteFileCommand(string markerPath, string marker) { if (OperatingSystem.IsWindows()) { - return $"powershell -NoLogo -NoProfile -Command \"Set-Content -LiteralPath '{markerPath}' -Value '{marker}'\""; + return $"echo {marker}>\"{Path.GetFileName(markerPath)}\""; } return $"sh -c \"printf '%s' '{marker}' > '{markerPath}'\""; diff --git a/dotnet/test/E2E/ScenarioTestingJsExtensionBridgeE2ETests.cs b/dotnet/test/E2E/ScenarioTestingJsExtensionBridgeE2ETests.cs index 89a50403e3..4ca3092e6b 100644 --- a/dotnet/test/E2E/ScenarioTestingJsExtensionBridgeE2ETests.cs +++ b/dotnet/test/E2E/ScenarioTestingJsExtensionBridgeE2ETests.cs @@ -272,7 +272,7 @@ await TestHelper.WaitForConditionAsync( }, timeout: ExtensionTimeout, pollInterval: TimeSpan.FromMilliseconds(100), - timeoutMessageFactory: () => $"Timed out waiting for extension '{extensionId}' (listed: [{lastExtensions}]; launch markers: {TestHelper.ExtensionLaunchMarkers(Ctx.HomeDir, extensionId)}).", + timeoutMessageFactory: () => $"Timed out waiting for extension '{extensionId}' (listed: [{lastExtensions}]; launch diagnostics: {TestHelper.ExtensionLaunchMarkers(Ctx.HomeDir, extensionId)}).", transientExceptionFilter: ex => ex.ToString().Contains("Extensions not available", StringComparison.OrdinalIgnoreCase)); return extension!; @@ -298,7 +298,7 @@ await TestHelper.WaitForConditionAsync( }, timeout: ExtensionTimeout, pollInterval: TimeSpan.FromMilliseconds(100), - timeoutMessageFactory: () => $"Timed out waiting for canvas from extension '{extensionId}' (listed: [{lastExtensions}]; launch markers: {TestHelper.ExtensionLaunchMarkers(Ctx.HomeDir, extensionId)})."); + timeoutMessageFactory: () => $"Timed out waiting for canvas from extension '{extensionId}' (listed: [{lastExtensions}]; launch diagnostics: {TestHelper.ExtensionLaunchMarkers(Ctx.HomeDir, extensionId)})."); return canvas!; } @@ -515,27 +515,32 @@ function record(kind, data = {}) { }); console.error("[sdk-extension-test] joining"); + let initializationStage = "join"; try { session = await joinSession({ workingDirectory, tools: [], canvases: [canvas] }); + console.error("[sdk-extension-test] joined"); + + initializationStage = "trace"; + record("joined", { + sessionId: session.sessionId, + workspacePath: session.workspacePath ?? null, + workingDirectory, + cwd: process.cwd() + }); + initializationStage = "log"; + await session.log("JS_EXTENSION_LOG"); + console.error("[sdk-extension-test] logged"); } catch (error) { const code = typeof error?.code === "number" ? error.code : "none"; - console.error(`[sdk-extension-test] join failed code=${code}`); + console.error(`[sdk-extension-test] ${initializationStage} failed code=${code}`); + const message = String(error?.message ?? error).replace(/\s+/g, " ").slice(0, 400); + process.stderr.write(`=== scenario-init-failed stage=${initializationStage} name=${error?.name ?? "unknown"} code=${error?.code ?? "none"} message=${message} ===\n`); throw error; } - console.error("[sdk-extension-test] joined"); - - record("joined", { - sessionId: session.sessionId, - workspacePath: session.workspacePath ?? null, - workingDirectory, - cwd: process.cwd() - }); - await session.log("JS_EXTENSION_LOG"); - console.error("[sdk-extension-test] logged"); setInterval(() => {}, 60_000).unref?.(); """; diff --git a/dotnet/test/E2E/SessionFsE2ETests.cs b/dotnet/test/E2E/SessionFsE2ETests.cs index 146e47d808..5751179a48 100644 --- a/dotnet/test/E2E/SessionFsE2ETests.cs +++ b/dotnet/test/E2E/SessionFsE2ETests.cs @@ -49,6 +49,48 @@ public async Task Should_Route_File_Operations_Through_The_Session_Fs_Provider() } } + [Fact] + // BYOK test models do not advertise vision; only the CAPI test model forwards + // the provider image to the model, which this replay capture verifies. + [Trait(E2ETestTraits.Backend, E2ETestTraits.CapiOnly)] + public async Task Should_View_An_Image_That_Exists_Only_In_The_Binary_Session_Fs_Provider() + { + const string imagePath = "/sdk-provider-image.png"; + var imageBytes = Convert.FromBase64String( + "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8DwHwAFBQIAX8jx0gAAAABJRU5ErkJggg=="); + var providerRoot = CreateProviderRoot(); + try + { + await using var client = CreateSessionFsClient(providerRoot, binary: true); + TestSessionFsHandler? provider = null; + var session = await Ctx.CreateSessionAsync(client, new SessionConfig + { + OnPermissionRequest = PermissionHandler.ApproveAll, + CreateSessionFsProvider = s => provider = new TestSessionFsHandler(s.SessionId, providerRoot), + }); + var storedPath = GetStoredPath(providerRoot, session.SessionId, imagePath); + Directory.CreateDirectory(Path.GetDirectoryName(storedPath)!); + await File.WriteAllBytesAsync(storedPath, imageBytes); + Assert.False(File.Exists(imagePath)); + + var msg = await session.SendAndWaitAsync(new MessageOptions + { + Prompt = "Use the view tool to view /sdk-provider-image.png, then reply with exactly SDK_PROVIDER_IMAGE_DONE.", + }); + Assert.Contains("SDK_PROVIDER_IMAGE_DONE", msg?.Data.Content ?? string.Empty); + Assert.Equal(imagePath, provider?.LastBinaryReadPath); + var events = await session.GetEventsAsync(); + Assert.Contains(events.OfType(), asset => + asset.Data.MimeType == "image/png" && + asset.Data.Data == Convert.ToBase64String(imageBytes)); + await session.DisposeAsync(); + } + finally + { + await TryDeleteDirectoryAsync(providerRoot); + } + } + [Fact] public async Task Should_Load_Session_Data_From_Fs_Provider_On_Resume() { @@ -280,7 +322,9 @@ public async Task SessionFsProvider_Converts_Exceptions_To_Rpc_Errors() var handler = (ISessionFsHandler)new ThrowingSessionFsProvider(new FileNotFoundException("missing")); AssertFsError((await handler.ReadFileAsync(new SessionFsReadFileRequest { Path = "missing.txt" })).Error); - AssertFsError(await handler.WriteFileAsync(new SessionFsWriteFileRequest { Path = "missing.txt", Content = "content" })); + var rejectedWrite = await handler.WriteFileAsync(new SessionFsWriteFileRequest { Path = "missing.txt", Content = "content" }); + AssertFsError(rejectedWrite); + Assert.Null(rejectedWrite?.WriteChanged); AssertFsError(await handler.AppendFileAsync(new SessionFsAppendFileRequest { Path = "missing.txt", Content = "content" })); var exists = await handler.ExistsAsync(new SessionFsExistsRequest { Path = "missing.txt" }); @@ -301,6 +345,12 @@ public async Task SessionFsProvider_Converts_Exceptions_To_Rpc_Errors() var unknownError = await unknown.WriteFileAsync(new SessionFsWriteFileRequest { Path = "bad.txt", Content = "content" }); Assert.Equal(SessionFsErrorCode.UNKNOWN, unknownError!.Code); + var partial = (ISessionFsHandler)new ThrowingSessionFsProvider(new SessionFsWriteException("disk full")); + var partialError = await partial.WriteFileAsync(new SessionFsWriteFileRequest { Path = "partial.txt", Content = "content" }); + Assert.Equal(SessionFsErrorCode.UNKNOWN, partialError?.Code); + Assert.True(partialError?.WriteChanged == true); + Assert.Null((await partial.ReadFileAsync(new SessionFsReadFileRequest { Path = "partial.txt" })).Error?.WriteChanged); + static void AssertFsError(SessionFsError? error) { Assert.NotNull(error); @@ -459,7 +509,7 @@ await WaitForConditionAsync( } } - private CopilotClient CreateSessionFsClient(string providerRoot, bool useStdio = true, string? tcpConnectionToken = null) + private CopilotClient CreateSessionFsClient(string providerRoot, bool useStdio = true, string? tcpConnectionToken = null, bool binary = false) { RuntimeConnection connection = useStdio ? RuntimeConnection.ForStdio() @@ -469,7 +519,15 @@ private CopilotClient CreateSessionFsClient(string providerRoot, bool useStdio = return Ctx.CreateClient( options: new CopilotClientOptions { - SessionFs = SessionFsConfig, + SessionFs = binary + ? new SessionFsConfig + { + InitialWorkingDirectory = SessionFsConfig.InitialWorkingDirectory, + SessionStatePath = SessionFsConfig.SessionStatePath, + Conventions = SessionFsConfig.Conventions, + Capabilities = new SessionFsSetProviderCapabilities { Binary = true }, + } + : SessionFsConfig, Connection = connection, }); } @@ -626,8 +684,24 @@ Task ISessionFsSqliteProvider.ExistsAsync(CancellationToken cancellationTo Task.FromException(exception); } - private sealed class TestSessionFsHandler(string sessionId, string rootDir) : SessionFsProvider + private sealed class TestSessionFsHandler(string sessionId, string rootDir) + : SessionFsProvider, ISessionFsBinaryProvider { + public string? LastBinaryReadPath { get; private set; } + + public Task ReadFileBytesAsync(string path, CancellationToken cancellationToken) + { + LastBinaryReadPath = path; + return File.ReadAllBytesAsync(ResolvePath(path), cancellationToken); + } + + public async Task WriteFileBytesAsync(string path, ReadOnlyMemory content, int? mode, CancellationToken cancellationToken) + { + var fullPath = ResolvePath(path); + Directory.CreateDirectory(Path.GetDirectoryName(fullPath)!); + await File.WriteAllBytesAsync(fullPath, content.ToArray(), cancellationToken); + } + protected override async Task ReadFileAsync(string path, CancellationToken cancellationToken) { return await File.ReadAllTextAsync(ResolvePath(path), cancellationToken); diff --git a/dotnet/test/E2E/SubagentHooksE2ETests.cs b/dotnet/test/E2E/SubagentHooksE2ETests.cs index c5d8c80fe2..fdbb779639 100644 --- a/dotnet/test/E2E/SubagentHooksE2ETests.cs +++ b/dotnet/test/E2E/SubagentHooksE2ETests.cs @@ -4,6 +4,7 @@ using System.Collections.Concurrent; using System.Net.Http; +using System.Text.Json; using GitHub.Copilot.Test.Harness; using Xunit; using Xunit.Abstractions; @@ -15,10 +16,16 @@ namespace GitHub.Copilot.Test.E2E; public class SubagentHooksE2ETests(E2ETestFixture fixture, ITestOutputHelper output) : E2ETestBase(fixture, "subagent_hooks", output) { + private const string ChildContext = "Subagent start hook verified: read the requested file."; + private const string StopResponsePrefix = "Subagent stop hook verified: "; + [Fact] - public async Task Should_Invoke_PreToolUse_And_PostToolUse_Hooks_For_Sub_Agent_Tool_Calls() + public async Task Should_Apply_Subagent_Lifecycle_Hook_Outputs() { var hookLog = new ConcurrentBag<(string Kind, string ToolName, string SessionId)>(); + var lifecycle = new ConcurrentQueue(); + var starts = new ConcurrentQueue<(SubagentStartHookInput Input, string InvocationSessionId)>(); + var stops = new ConcurrentQueue<(SubagentStopHookInput Input, string InvocationSessionId)>(); var requestHandler = new RecordingForwardingRequestHandler(); const string waitingText = "I've launched an explore agent to read subagent-test.txt. Waiting for it to complete..."; const string finalText = "The explore agent successfully read the file. The contents of **subagent-test.txt** are:\n\n```\nHello from subagent test!\n```"; @@ -58,6 +65,24 @@ public async Task Should_Invoke_PreToolUse_And_PostToolUse_Hooks_For_Sub_Agent_T } return null; }, + OnSubagentStart = (input, invocation) => + { + starts.Enqueue((input, invocation.SessionId)); + lifecycle.Enqueue("start"); + return Task.FromResult(new SubagentStartHookOutput + { + AdditionalContext = ChildContext + }); + }, + OnSubagentStop = (input, invocation) => + { + stops.Enqueue((input, invocation.SessionId)); + lifecycle.Enqueue("stop"); + return Task.FromResult(new SubagentStopHookOutput + { + ModifiedResponse = StopResponsePrefix + input.Response + }); + }, }, }); @@ -95,6 +120,34 @@ public async Task Should_Invoke_PreToolUse_And_PostToolUse_Hooks_For_Sub_Agent_T parentWaiting.TrySetResult(true); } + Assert.Collection(lifecycle, + kind => Assert.Equal("start", kind), + kind => Assert.Equal("stop", kind)); + + var start = Assert.Single(starts); + Assert.Equal(session.SessionId, start.InvocationSessionId); + Assert.Equal(session.SessionId, start.Input.SessionId); + Assert.True(start.Input.Timestamp > DateTimeOffset.UnixEpoch); + Assert.Equal(Path.GetFullPath(Ctx.WorkDir), Path.GetFullPath(start.Input.WorkingDirectory)); + Assert.Equal("explore", start.Input.AgentName); + Assert.Null(start.Input.AgentDisplayName); + Assert.Null(start.Input.AgentDescription); + + var stop = Assert.Single(stops); + Assert.Equal(session.SessionId, stop.InvocationSessionId); + Assert.Equal(session.SessionId, stop.Input.SessionId); + Assert.True(stop.Input.Timestamp > DateTimeOffset.UnixEpoch); + Assert.True(stop.Input.Timestamp >= start.Input.Timestamp); + Assert.Equal(Path.GetFullPath(Ctx.WorkDir), Path.GetFullPath(stop.Input.WorkingDirectory)); + Assert.Equal(start.Input.TranscriptPath, stop.Input.TranscriptPath); + Assert.Equal(start.Input.AgentName, stop.Input.AgentName); + Assert.Equal(start.Input.AgentDisplayName, stop.Input.AgentDisplayName); + Assert.Equal(start.Input.AgentDescription, stop.Input.AgentDescription); + Assert.False(string.IsNullOrEmpty(stop.Input.AgentId)); + Assert.Equal("explore", stop.Input.AgentType); + Assert.Equal("end_turn", stop.Input.StopReason); + Assert.Contains("Hello from subagent test!", stop.Input.Response); + var log = hookLog.ToArray(); // Parent tool hooks fire for "task" @@ -110,6 +163,30 @@ public async Task Should_Invoke_PreToolUse_And_PostToolUse_Hooks_For_Sub_Agent_T // input.SessionId distinguishes parent from sub-agent Assert.NotEqual(viewPre[0].SessionId, taskPre[0].SessionId); AssertSubagentRequestMetadata(requestHandler.InferenceRequests); + Assert.Contains(requestHandler.InferenceRequests, r => + { + if (string.IsNullOrEmpty(r.ParentAgentId)) return false; + using var body = JsonDocument.Parse(r.Body); + if (!body.RootElement.TryGetProperty("messages", out var messages) + && !body.RootElement.TryGetProperty("input", out messages)) + return false; + return messages.ValueKind == JsonValueKind.Array && messages.EnumerateArray().Any(message => + message.ValueKind == JsonValueKind.Object + && message.TryGetProperty("role", out var role) + && role.ValueKind == JsonValueKind.String + && role.GetString() == "user" + && message.TryGetProperty("content", out var content) + && (content.ValueKind == JsonValueKind.String + ? content.GetString()!.Contains(ChildContext + "\n\nRead the file \"subagent-test.txt\"", StringComparison.Ordinal) + : content.ValueKind == JsonValueKind.Array && content.EnumerateArray().Any(part => + part.ValueKind == JsonValueKind.Object + && part.TryGetProperty("text", out var text) + && text.ValueKind == JsonValueKind.String + && text.GetString()!.Contains(ChildContext + "\n\nRead the file \"subagent-test.txt\"", StringComparison.Ordinal)))); + }); + Assert.Contains(requestHandler.InferenceRequests, r => + string.IsNullOrEmpty(r.ParentAgentId) + && r.Body.Contains(StopResponsePrefix, StringComparison.Ordinal)); } private static void AssertSubagentRequestMetadata(IReadOnlyCollection records) @@ -131,14 +208,18 @@ private sealed class RecordingForwardingRequestHandler : CopilotRequestHandler public IReadOnlyCollection InferenceRequests => [.. _records.Where(r => RecordingRequestHandler.IsInferenceUrl(r.Url))]; - protected override Task SendRequestAsync(HttpRequestMessage request, CopilotRequestContext ctx) + protected override async Task SendRequestAsync(HttpRequestMessage request, CopilotRequestContext ctx) { + var body = RecordingRequestHandler.IsInferenceUrl(request.RequestUri!.ToString()) + ? await request.Content!.ReadAsStringAsync() + : string.Empty; _records.Add(new RequestRecord( request.RequestUri!.ToString(), ctx.AgentId, ctx.ParentAgentId, - ctx.InteractionType)); - return base.SendRequestAsync(request, ctx); + ctx.InteractionType, + body)); + return await base.SendRequestAsync(request, ctx); } } @@ -146,5 +227,6 @@ private sealed record RequestRecord( string Url, string? AgentId, string? ParentAgentId, - string? InteractionType); + string? InteractionType, + string Body); } diff --git a/dotnet/test/E2E/ToolsE2ETests.cs b/dotnet/test/E2E/ToolsE2ETests.cs index 943e49f45f..e5714848ad 100644 --- a/dotnet/test/E2E/ToolsE2ETests.cs +++ b/dotnet/test/E2E/ToolsE2ETests.cs @@ -319,6 +319,67 @@ string CaptureInput(object input) } } + [Theory] + [InlineData(true)] + [InlineData(false)] + [Trait(E2ETestTraits.Backend, E2ETestTraits.SelfConfiguredBackend)] + public async Task ApplyPatch_StringSchemaOverride_Binds_Patch_Input(bool useCustomToolCall) + { + var receivedInput = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + JsonElement? receivedArguments = null; + var invocationCount = 0; + var handler = new ApplyPatchOverrideRequestHandler(useCustomToolCall); + await using var client = Ctx.CreateClient(options: new CopilotClientOptions + { + Connection = RuntimeConnection.ForStdio(), + RequestHandler = handler, + }); + await client.StartAsync(); + + var inner = CopilotTool.DefineTool( + (string input, ToolInvocation invocation) => + { + receivedArguments = invocation.Arguments; + Interlocked.Increment(ref invocationCount); + receivedInput.TrySetResult(input); + return ApplyPatchResult; + }, + new CopilotToolOptions { OverridesBuiltInTool = true, SkipPermission = true }, + new AIFunctionFactoryOptions { Name = "apply_patch", Description = "Host-implemented apply_patch" }); + var tool = new StringSchemaApplyPatchTool(inner); + Assert.Equal("string", tool.JsonSchema.GetProperty("type").GetString()); + + await using var session = await Ctx.CreateSessionAsync(client, new SessionConfig + { + Model = "gpt-4o-mini", + Provider = new ProviderConfig + { + Type = "openai", + WireApi = "completions", + BaseUrl = "https://apply-patch.invalid/v1", + ApiKey = "test-key", + ModelId = "gpt-4o-mini", + WireModel = "gpt-4o-mini", + }, + Streaming = true, + Tools = [tool], + OnPermissionRequest = PermissionHandler.ApproveAll, + }); + + var message = await session.SendAndWaitAsync(new MessageOptions { Prompt = "Use apply_patch" }); + + Assert.True(receivedInput.Task.IsCompleted); + Assert.Equal(ApplyPatchInput, await receivedInput.Task); + Assert.Equal(JsonValueKind.String, receivedArguments?.ValueKind); + Assert.Equal(ApplyPatchInput, receivedArguments?.GetString()); + Assert.Equal(1, Volatile.Read(ref invocationCount)); + Assert.Equal("override complete", message?.Data.Content); + var requests = handler.InferenceRequests; + Assert.Equal(2, requests.Count); + AssertApplyPatchOverrideAdvertised(requests[0], "string"); + AssertApplyPatchResultReachedModel(requests[1]); + } + [Fact] public async Task SkipPermission_Sent_In_Tool_Definition() { @@ -573,6 +634,17 @@ private static void AssertApplyPatchResultReachedModel(string requestBody) Assert.Equal(ApplyPatchResult, toolResult.GetProperty("content").GetString()); } + private sealed class StringSchemaApplyPatchTool(AIFunction inner) : DelegatingAIFunction(inner) + { + public override JsonElement JsonSchema { get; } = CreateSchema(); + + private static JsonElement CreateSchema() + { + using var document = JsonDocument.Parse("""{"type":"string"}"""); + return document.RootElement.Clone(); + } + } + private sealed class ApplyPatchOverrideRequestHandler(bool useCustomToolCall) : CopilotRequestHandler { private const string CustomToolCallResponse = diff --git a/dotnet/test/Harness/ReplayProxy.cs b/dotnet/test/Harness/ReplayProxy.cs index 0f9bd1dc4c..140642a2f7 100644 --- a/dotnet/test/Harness/ReplayProxy.cs +++ b/dotnet/test/Harness/ReplayProxy.cs @@ -40,18 +40,14 @@ async Task StartCoreAsync() }; var process = new Process { StartInfo = startInfo }; - - var tcs = new TaskCompletionSource(); - var errorOutput = new StringBuilder(); - var startupTimer = Stopwatch.StartNew(); - string CapturedErrors() - { - lock (errorOutput) return errorOutput.ToString(); - } + var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + var diagnostics = new StartupDiagnostics(); + var elapsed = Stopwatch.StartNew(); process.OutputDataReceived += (_, e) => { if (e.Data == null) return; + diagnostics.AppendOutput(e.Data); var match = Regex.Match(e.Data, @"Listening: (?http://[^\s]+)\s+(?\{.*\})$"); if (!match.Success) { @@ -92,7 +88,7 @@ string CapturedErrors() process.ErrorDataReceived += (_, e) => { if (e.Data == null) return; - lock (errorOutput) errorOutput.AppendLine(e.Data); + diagnostics.AppendError(e.Data); Console.Error.WriteLine(e.Data); }; @@ -112,21 +108,56 @@ string CapturedErrors() { if (!tcs.Task.IsCompleted) { - tcs.TrySetException(new Exception($"Proxy exited before listening with code {process.ExitCode}: {CapturedErrors()}")); + tcs.TrySetException(new Exception( + $"Proxy exited before readiness. {diagnostics.Describe(process.Id, elapsed.Elapsed, process.ExitCode)}")); } }); // Use longer timeout on Windows due to slower process startup var timeoutSeconds = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? 30 : 10; using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(timeoutSeconds)); - cts.Token.Register(() => tcs.TrySetException(new TimeoutException( - $"Timeout waiting for proxy after {startupTimer.ElapsedMilliseconds}ms " + - $"(exited: {process.HasExited}; stderr: {CapturedErrors()})"))); + using var registration = cts.Token.Register(() => tcs.TrySetException(new TimeoutException( + $"Timeout waiting for proxy. {diagnostics.Describe(process.Id, elapsed.Elapsed, process.HasExited ? process.ExitCode : null)}"))); return await tcs.Task; } } + internal sealed class StartupDiagnostics + { + private const int OutputLimit = 4096; + private readonly object _gate = new(); + private readonly StringBuilder _stdout = new(); + private readonly StringBuilder _stderr = new(); + + public void AppendOutput(string line) => Append(_stdout, line); + + public void AppendError(string line) => Append(_stderr, line); + + private void Append(StringBuilder output, string line) + { + lock (_gate) + { + output.Append(line, Math.Max(0, line.Length - OutputLimit), Math.Min(line.Length, OutputLimit)); + output.AppendLine(); + if (output.Length > OutputLimit) + { + output.Remove(0, output.Length - OutputLimit); + } + } + } + + public string Describe(int processId, TimeSpan elapsed, int? exitCode) + { + lock (_gate) + { + var state = exitCode is int code ? $"exited with code {code}" : "running"; + return $"PID {processId}, {state}, elapsed {elapsed.TotalSeconds:F1}s." + + $"\n--- stdout tail ---\n{_stdout}\n--- stderr tail ---\n{_stderr}"; + } + } + } + public async Task StopAsync(bool skipWritingCache = false) { if (_startupTask != null) @@ -256,7 +287,10 @@ public record CopilotUserQuotaSnapshot( public record ParsedHttpExchange( ChatCompletionRequest Request, ChatCompletionResponse? Response, - Dictionary? RequestHeaders); + Dictionary? RequestHeaders, + CompactionProviderUsage? CompactionUsage = null); + +public record CompactionProviderUsage(string InteractionId, string Summary, int ResponseCount, long? InputTokens); public record ChatCompletionRequest( string Model, diff --git a/dotnet/test/Harness/TestHelper.cs b/dotnet/test/Harness/TestHelper.cs index 173a5f45f5..1a7773dc21 100644 --- a/dotnet/test/Harness/TestHelper.cs +++ b/dotnet/test/Harness/TestHelper.cs @@ -141,7 +141,8 @@ public static string ExtensionLaunchMarkers(string homeDir, string extensionId) var launches = new List(); foreach (var path in Directory.EnumerateFiles(logsDir, "process-*.log")) { - using var reader = new StreamReader(path); + using var stream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite | FileShare.Delete); + using var reader = new StreamReader(stream); if (reader.ReadLine()?.Contains(extensionName, StringComparison.Ordinal) != true) { continue; @@ -149,14 +150,29 @@ public static string ExtensionLaunchMarkers(string homeDir, string extensionId) var markers = new List(); var errors = new HashSet(StringComparer.Ordinal); + var recentLines = new Queue(); while (reader.ReadLine() is { } line) { + if (recentLines.Count == 20) + { + recentLines.Dequeue(); + } + recentLines.Enqueue(line); + if (line.StartsWith("=== ", StringComparison.Ordinal)) { - if (!line.Contains("module=", StringComparison.Ordinal)) + if (line is "=== initializing ===" or "=== ready ===" or "=== peer-registration-failed ===" + or "=== exited-before-ready ===" or "=== startup-timeout ===" + or "=== peer-closed-before-ready ===") { markers.Add(line); } + var exit = Regex.Match(line, @"^=== exit code=(-?\d+|signal) disposition=(startup-failure|crash|stopped-normally|termination-failure) ===$"); + if (exit.Success) + { + var code = exit.Groups[1].Value; + markers.Add($"=== exit code={(code is "0" or "1" or "signal" ? code : "other")} disposition={exit.Groups[2].Value} ==="); + } continue; } @@ -181,28 +197,31 @@ public static string ExtensionLaunchMarkers(string homeDir, string extensionId) } if (line is "[sdk-extension-test] imported" or "[sdk-extension-test] joining" - or "[sdk-extension-test] joined" or "[sdk-extension-test] logged" - || Regex.IsMatch(line, @"^\[sdk-extension-test\] join failed code=(?:-?\d+|none)$")) + or "[sdk-extension-test] joined" or "[sdk-extension-test] logged") { markers.Add(line); } - - var code = Regex.Match(line, @"\b(?:ERR_[A-Z0-9_]+|ECONNRESET|ECONNREFUSED|EPIPE|ENOENT|ETIMEDOUT)\b"); - if (code.Success) - { - errors.Add(code.Value); - } - var type = Regex.Match(line, @"(? 0) { summary += $"; error categories: {string.Join(", ", errors.OrderBy(error => error, StringComparer.Ordinal))}"; } + var recentErrors = new HashSet(StringComparer.Ordinal); + foreach (var line in recentLines) + { + AddErrorCategories(line, recentErrors); + } + if (recentErrors.Count > 0) + { + summary += $"; recent error categories: {string.Join(", ", recentErrors.OrderBy(error => error, StringComparer.Ordinal))}"; + } launches.Add(summary.Length == 0 ? "" : summary); } @@ -213,4 +232,20 @@ public static string ExtensionLaunchMarkers(string homeDir, string extensionId) return $""; } } + + private static void AddErrorCategories(string line, HashSet errors) + { + var code = Regex.Match(line, @"\b(?:ERR_[A-Z0-9_]+|ECONNRESET|ECONNREFUSED|EPIPE|ENOENT|ETIMEDOUT)\b"); + if (code.Success) + { + errors.Add(code.Value.StartsWith("ERR_", StringComparison.Ordinal) && code.Value != "ERR_INVALID_ARG_TYPE" + ? "Node.js error" + : code.Value); + } + var type = Regex.Match(line, @"(?(); + var hooks = new SessionHooks + { + OnSubagentStart = (input, invocation) => + { + calls.Add((input, invocation.SessionId)); + return Task.FromResult(new SubagentStartHookOutput + { + AdditionalContext = "Context for the child" + }); + } + }; + await using var session = resume + ? await client.ResumeSessionAsync("resumed-session", new ResumeSessionConfig { Hooks = hooks }) + : await client.CreateSessionAsync(new SessionConfig { Hooks = hooks }); + + var registration = Assert.Single(server.Requests, r => r.Method == (resume ? "session.resume" : "session.create")); + Assert.True(registration.Params.GetProperty("hooks").GetBoolean()); + + var input = new Dictionary + { + ["sessionId"] = session.SessionId, + ["timestamp"] = 1_730_000_000_000L, + ["cwd"] = "workdir", + ["transcriptPath"] = "parent.jsonl", + ["agentName"] = "explore", + ["agentDisplayName"] = "Explorer", + ["agentDescription"] = "Reads files" + }; + var request = new Dictionary + { + ["sessionId"] = session.SessionId, + ["hookType"] = "subagentStart", + ["input"] = input + }; + var result = await server.SendRequestAsync("hooks.invoke", request).WaitAsync(TimeSpan.FromSeconds(5)); + var output = result.GetProperty("output"); + var property = Assert.Single(output.EnumerateObject()); + Assert.Equal("additionalContext", property.Name); + Assert.Equal("Context for the child", property.Value.GetString()); + + var (first, invocationSessionId) = Assert.Single(calls); + Assert.Equal(session.SessionId, invocationSessionId); + Assert.Equal(session.SessionId, first.SessionId); + Assert.Equal(DateTimeOffset.FromUnixTimeMilliseconds(1_730_000_000_000L), first.Timestamp); + Assert.Equal("workdir", first.WorkingDirectory); + Assert.Equal("parent.jsonl", first.TranscriptPath); + Assert.Equal("explore", first.AgentName); + Assert.Equal("Explorer", first.AgentDisplayName); + Assert.Equal("Reads files", first.AgentDescription); + + input.Remove("agentDisplayName"); + input.Remove("agentDescription"); + await server.SendRequestAsync("hooks.invoke", request).WaitAsync(TimeSpan.FromSeconds(5)); + Assert.Equal(2, calls.Count); + Assert.Null(calls[1].Input.AgentDisplayName); + Assert.Null(calls[1].Input.AgentDescription); + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task SubagentStopHook_RegistersOnCreateAndResume_AndMapsInputAndBothOutputs(bool resume) + { + await using var server = await FakeCopilotServer.StartAsync(); + await using var client = new CopilotClient(new CopilotClientOptions { Connection = RuntimeConnection.ForUri(server.Url) }); + var calls = new List<(SubagentStopHookInput Input, string InvocationSessionId)>(); + var block = true; + var hooks = new SessionHooks + { + OnSubagentStop = (input, invocation) => + { + calls.Add((input, invocation.SessionId)); + return Task.FromResult(block + ? new SubagentStopHookOutput { Decision = "block", Reason = "Continue reading" } + : new SubagentStopHookOutput { ModifiedResponse = "Replaced child response" }); + } + }; + await using var session = resume + ? await client.ResumeSessionAsync("resumed-session", new ResumeSessionConfig { Hooks = hooks }) + : await client.CreateSessionAsync(new SessionConfig { Hooks = hooks }); + + var registration = Assert.Single(server.Requests, r => r.Method == (resume ? "session.resume" : "session.create")); + Assert.True(registration.Params.GetProperty("hooks").GetBoolean()); + + var input = new Dictionary + { + ["sessionId"] = session.SessionId, + ["timestamp"] = 1_730_000_000_000L, + ["cwd"] = "workdir", + ["transcriptPath"] = "parent.jsonl", + ["agentName"] = "explore", + ["agentDisplayName"] = "Explorer", + ["agentDescription"] = "Reads files", + ["agentId"] = "read-file", + ["agentType"] = "explore", + ["stopReason"] = "end_turn", + ["response"] = "Hello from subagent test!" + }; + var request = new Dictionary + { + ["sessionId"] = session.SessionId, + ["hookType"] = "subagentStop", + ["input"] = input + }; + var blocked = await server.SendRequestAsync("hooks.invoke", request).WaitAsync(TimeSpan.FromSeconds(5)); + var blockOutput = blocked.GetProperty("output"); + Assert.Equal(2, blockOutput.EnumerateObject().Count()); + Assert.Equal("block", blockOutput.GetProperty("decision").GetString()); + Assert.Equal("Continue reading", blockOutput.GetProperty("reason").GetString()); + Assert.False(blockOutput.TryGetProperty("modifiedResponse", out _)); + + var (first, invocationSessionId) = Assert.Single(calls); + Assert.Equal(session.SessionId, invocationSessionId); + Assert.Equal(session.SessionId, first.SessionId); + Assert.Equal(DateTimeOffset.FromUnixTimeMilliseconds(1_730_000_000_000L), first.Timestamp); + Assert.Equal("workdir", first.WorkingDirectory); + Assert.Equal("parent.jsonl", first.TranscriptPath); + Assert.Equal("explore", first.AgentName); + Assert.Equal("Explorer", first.AgentDisplayName); + Assert.Equal("Reads files", first.AgentDescription); + Assert.Equal("read-file", first.AgentId); + Assert.Equal("explore", first.AgentType); + Assert.Equal("end_turn", first.StopReason); + Assert.Equal("Hello from subagent test!", first.Response); + + input.Remove("agentId"); + input.Remove("agentDisplayName"); + input.Remove("agentDescription"); + block = false; + var modified = await server.SendRequestAsync("hooks.invoke", request).WaitAsync(TimeSpan.FromSeconds(5)); + var modifiedOutput = modified.GetProperty("output"); + var property = Assert.Single(modifiedOutput.EnumerateObject()); + Assert.Equal("modifiedResponse", property.Name); + Assert.Equal("Replaced child response", property.Value.GetString()); + Assert.Equal(2, calls.Count); + Assert.Null(calls[1].Input.AgentId); + Assert.Null(calls[1].Input.AgentDisplayName); + Assert.Null(calls[1].Input.AgentDescription); + } + [Theory] [InlineData("static")] [InlineData("")] @@ -1308,6 +1456,140 @@ public async Task CreateSessionAsync_Registers_McpAuth_Interest_After_Cloud_Crea }); } + [Fact] + public async Task CreateSessionAsync_Preserves_Server_Assigned_Session_When_McpAuth_Interest_Fails() + { + await using var server = await FakeCopilotServer.StartAsync(); + server.FailMcpAuthInterestRegistration = true; + await using var client = new CopilotClient(new CopilotClientOptions { Connection = RuntimeConnection.ForUri(server.Url) }); + + var error = await Assert.ThrowsAsync(() => client.CreateSessionAsync(new SessionConfig + { + Cloud = new CloudSessionOptions + { + Repository = new CloudSessionRepository { Owner = "github", Name = "copilot-sdk", Branch = "main" } + }, + OnMcpAuthRequest = _ => Task.FromResult(McpAuthResult.Cancel()) + })); + + Assert.Contains("interest registration failed", error.Message); + var create = Assert.Single(server.Requests, request => request.Method == "session.create"); + Assert.False(create.Params.TryGetProperty("sessionId", out _)); + Assert.NotNull(server.LastSessionId); + var interest = Assert.Single(server.Requests, request => request.Method == "session.eventLog.registerInterest"); + Assert.Equal(server.LastSessionId, interest.Params.GetProperty("sessionId").GetString()); + Assert.DoesNotContain(server.Requests, request => request.Method == "session.delete"); + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task CreateSessionAsync_Deletes_Server_Assigned_Session_When_Binary_Provider_Validation_Fails(bool failSessionDelete) + { + await using var server = await FakeCopilotServer.StartAsync(); + server.FailSessionDelete = failSessionDelete; + server.ResponseFactory = request => request.Method == "sessionFs.setProvider" + ? new Dictionary { ["success"] = true } + : throw new InvalidOperationException($"Unexpected RPC method '{request.Method}'."); + await using var client = new CopilotClient(new CopilotClientOptions + { + Connection = RuntimeConnection.ForUri(server.Url), + SessionFs = new SessionFsConfig + { + InitialWorkingDirectory = "/", + SessionStatePath = "/session-state", + Conventions = SessionFsSetProviderConventions.Posix, + Capabilities = new SessionFsSetProviderCapabilities { Binary = true } + } + }); + + var error = await Assert.ThrowsAsync(() => client.CreateSessionAsync(new SessionConfig + { + Cloud = new CloudSessionOptions + { + Repository = new CloudSessionRepository { Owner = "github", Name = "copilot-sdk", Branch = "main" } + }, + CreateSessionFsProvider = _ => new TextOnlySessionFsProvider() + })); + + Assert.Contains("does not implement ISessionFsBinaryProvider", error.Message); + var create = Assert.Single(server.Requests, request => request.Method == "session.create"); + Assert.False(create.Params.TryGetProperty("sessionId", out _)); + var delete = Assert.Single(server.Requests, request => request.Method == "session.delete"); + Assert.NotNull(server.LastSessionId); + Assert.Equal(server.LastSessionId, delete.Params.GetProperty("sessionId").GetString()); + } + + [Fact] + public async Task Binary_SessionFs_Provider_Rejects_Oversized_Bytes_Before_Encoding() + { + var provider = new BinarySessionFsProvider(_ => Task.FromResult(new byte[(64 * 1024 * 1024 - 1024) / 4 * 3 + 1])); + var result = await ((ISessionFsHandler)provider).ReadFileBytesAsync( + new SessionFsReadFileBytesRequest { SessionId = "session", Path = "/image.png" }, CancellationToken.None); + + Assert.Empty(result.Content); + Assert.NotNull(result.Error); + Assert.Equal(SessionFsErrorCode.UNKNOWN, result.Error.Code); + Assert.Contains("binary read limit", result.Error.Message); + } + + [Fact] + public async Task Binary_SessionFs_Provider_Propagates_Cancellation() + { + var provider = new BinarySessionFsProvider(token => Task.FromCanceled(token)); + var cancellationToken = new CancellationToken(canceled: true); + await Assert.ThrowsAnyAsync(() => ((ISessionFsHandler)provider).ReadFileBytesAsync( + new SessionFsReadFileBytesRequest { SessionId = "session", Path = "/image.png" }, cancellationToken)); + } + + [Fact] + public async Task Binary_SessionFs_Provider_Writes_Exact_Bytes_And_Rejects_Invalid_Input() + { + ReadOnlyMemory? received = null; + int? receivedMode = null; + var provider = new BinarySessionFsProvider( + _ => Task.FromResult(Array.Empty()), + (content, mode) => { received = content; receivedMode = mode; return Task.CompletedTask; }); + var handler = (ISessionFsHandler)provider; + var result = await handler.WriteFileBytesAsync( + new SessionFsWriteFileBytesRequest { SessionId = "session", Path = "/image.png", Content = "AP/+AQ==", Mode = 384 }, + CancellationToken.None); + Assert.Null(result); + Assert.Equal(new byte[] { 0, 255, 254, 1 }, received?.ToArray()); + Assert.Equal(384, receivedMode); + + foreach (var content in new[] { "AA==AAAA", "AA==\r\n", new string('A', 64 * 1024 * 1024) }) + { + var error = await handler.WriteFileBytesAsync( + new SessionFsWriteFileBytesRequest { SessionId = "session", Path = "/image.png", Content = content }, + CancellationToken.None); + Assert.NotNull(error); + Assert.Equal(new byte[] { 0, 255, 254, 1 }, received?.ToArray()); + } + } + + private sealed class BinarySessionFsProvider(Func> read, Func, int?, Task>? write = null) + : TextOnlySessionFsProvider, ISessionFsBinaryProvider + { + public Task ReadFileBytesAsync(string path, CancellationToken cancellationToken) => read(cancellationToken); + public Task WriteFileBytesAsync(string path, ReadOnlyMemory content, int? mode, CancellationToken cancellationToken) => + write?.Invoke(content, mode) ?? Task.CompletedTask; + } + + private class TextOnlySessionFsProvider : SessionFsProvider + { + protected override Task ReadFileAsync(string path, CancellationToken cancellationToken) => throw new NotSupportedException(); + protected override Task WriteFileAsync(string path, string content, int? mode, CancellationToken cancellationToken) => throw new NotSupportedException(); + protected override Task AppendFileAsync(string path, string content, int? mode, CancellationToken cancellationToken) => throw new NotSupportedException(); + protected override Task ExistsAsync(string path, CancellationToken cancellationToken) => throw new NotSupportedException(); + protected override Task StatAsync(string path, CancellationToken cancellationToken) => throw new NotSupportedException(); + protected override Task MakeDirectoryAsync(string path, bool recursive, int? mode, CancellationToken cancellationToken) => throw new NotSupportedException(); + protected override Task> ReadDirectoryAsync(string path, CancellationToken cancellationToken) => throw new NotSupportedException(); + protected override Task> ReadDirectoryWithTypesAsync(string path, CancellationToken cancellationToken) => throw new NotSupportedException(); + protected override Task RemoveAsync(string path, bool recursive, bool force, CancellationToken cancellationToken) => throw new NotSupportedException(); + protected override Task RenameAsync(string src, string dest, CancellationToken cancellationToken) => throw new NotSupportedException(); + } + [Fact] public async Task ResumeSessionAsync_Registers_McpAuth_Interest_Only_When_Handler_Configured() { @@ -2739,7 +3021,10 @@ private sealed class FakeCopilotServer : IAsyncDisposable private int _nextMessageId; public bool IncludeTranscriptRecovery { get; set; } + public string? LastSessionId => _lastSessionId; public bool UniqueMessageIds { get; set; } + public bool FailSessionDelete { get; set; } + public bool FailMcpAuthInterestRegistration { get; set; } public Func? BeforeSendResponse { get; set; } @@ -3006,6 +3291,20 @@ private async Task HandleRequestAsync(Stream stream, JsonElement request, Cancel }, cancellationToken); return; } + if (method == "session.eventLog.registerInterest" && FailMcpAuthInterestRegistration) + { + await WriteMessageAsync(stream, new Dictionary + { + ["jsonrpc"] = "2.0", + ["id"] = id, + ["error"] = new Dictionary + { + ["code"] = -32000, + ["message"] = "interest registration failed" + } + }, cancellationToken); + return; + } if (method == "session.send" && _failSessionSend) { _failSessionSend = false; @@ -3092,7 +3391,7 @@ private async Task HandleRequestAsync(Stream stream, JsonElement request, Cancel }, "session.delete" => new Dictionary { - ["success"] = true + ["success"] = !FailSessionDelete }, "session.detach" => await DetachSessionAsync(cancellationToken), "runtime.shutdown" => HandleRuntimeShutdown(), diff --git a/dotnet/test/Unit/E2ETestFixtureTests.cs b/dotnet/test/Unit/E2ETestFixtureTests.cs index e2d7cd0723..933b82c1fc 100644 --- a/dotnet/test/Unit/E2ETestFixtureTests.cs +++ b/dotnet/test/Unit/E2ETestFixtureTests.cs @@ -142,4 +142,38 @@ public async Task Replay_Proxy_Starts_With_Connect_Metadata_And_Stops() await proxy.StopAsync(skipWritingCache: true); } } + + [Theory] + [InlineData(false, null)] + [InlineData(true, 0)] + [InlineData(true, 1)] + public void Proxy_Startup_Diagnostics_Preserve_Process_State_And_Bounded_Output(bool largeOutput, int? exitCode) + { + var output = new ReplayProxy.StartupDiagnostics(); + output.AppendOutput("old stdout"); + output.AppendError("old stderr"); + if (largeOutput) + { + output.AppendOutput(new string('x', 12_000)); + output.AppendError(new string('y', 12_000)); + } + output.AppendOutput("tsx server.ts"); + output.AppendError("proxy startup error"); + + var diagnostics = output.Describe(1234, TimeSpan.FromSeconds(30), exitCode); + + Assert.Contains("PID 1234", diagnostics); + Assert.Contains(exitCode is int code ? $"exited with code {code}" : "running", diagnostics); + Assert.Contains("--- stdout tail ---", diagnostics); + Assert.Contains("tsx server.ts", diagnostics); + Assert.Contains("--- stderr tail ---", diagnostics); + Assert.Contains("proxy startup error", diagnostics); + if (largeOutput) + { + Assert.DoesNotContain("old stdout", diagnostics); + Assert.DoesNotContain("old stderr", diagnostics); + } + Assert.True(diagnostics.Length < 8500, $"Diagnostic length: {diagnostics.Length}"); + } + } diff --git a/dotnet/test/Unit/ExtensionLaunchDiagnosticsTests.cs b/dotnet/test/Unit/ExtensionLaunchDiagnosticsTests.cs index af70515c9f..a13931b42b 100644 --- a/dotnet/test/Unit/ExtensionLaunchDiagnosticsTests.cs +++ b/dotnet/test/Unit/ExtensionLaunchDiagnosticsTests.cs @@ -9,6 +9,44 @@ namespace GitHub.Copilot.Test.Unit; public class ExtensionLaunchDiagnosticsTests { + [Fact] + public void Reports_Recent_Error_Categories_Without_Exposing_Tail_Lines() + { + var homeDir = Path.Join(Path.GetTempPath(), $"extension-diagnostics-{Guid.NewGuid():N}"); + var logsDir = Path.Join(homeDir, "logs"); + Directory.CreateDirectory(logsDir); + try + { + File.WriteAllLines(Path.Join(logsDir, "process-1.log"), + [ + "=== module=/private/project/sample-extension/extension.mjs ===", + "TypeError: old failure", + .. Enumerable.Range(0, 20).Select(index => $"context {index}"), + "Error: connect ECONNREFUSED to secret-host.example using secret-user-input", + "=== secret-user-input ===", + "=== exit code=123456789 disposition=crash ===", + "[sdk-extension-test] join failed code=123456789", + "Error [ERR_SECRET_USER_INPUT]: secret-user-input", + ]); + + var summary = TestHelper.ExtensionLaunchMarkers(homeDir, "user:sample-extension"); + + Assert.Contains("recent error categories: ECONNREFUSED, Error", summary); + Assert.DoesNotContain("TypeError", summary.Split(["recent error categories:"], StringSplitOptions.None)[1]); + Assert.DoesNotContain("secret-user-input", summary); + Assert.DoesNotContain("ERR_SECRET_USER_INPUT", summary); + Assert.Contains("=== exit code=other disposition=crash ===", summary); + Assert.Contains("[sdk-extension-test] join failed", summary); + Assert.DoesNotContain("123456789", summary); + Assert.DoesNotContain("secret-host.example", summary); + Assert.DoesNotContain("/private/project", summary); + } + finally + { + Directory.Delete(homeDir, recursive: true); + } + } + [Fact] public void Reports_Exception_Categories_Without_Exposing_Child_Stderr() { @@ -25,6 +63,12 @@ public void Reports_Exception_Categories_Without_Exposing_Child_Stderr() Error: connect ECONNRESET to private-host.example === exit code=1 disposition=crash === """); + File.WriteAllText(Path.Join(logsDir, "process-2.log"), """ + === module=/private/project/other-extension/extension.mjs === + ReferenceError: unrelated-child-error + """); + using var activeWriter = new FileStream( + Path.Join(logsDir, "process-1.log"), FileMode.Open, FileAccess.Write, FileShare.ReadWrite | FileShare.Delete); var summary = TestHelper.ExtensionLaunchMarkers(homeDir, "user:sample-extension"); @@ -38,6 +82,7 @@ public void Reports_Exception_Categories_Without_Exposing_Child_Stderr() Assert.DoesNotContain("secret-user-input", summary); Assert.DoesNotContain("private-host.example", summary); Assert.DoesNotContain("/private/project", summary); + Assert.DoesNotContain("ReferenceError", summary); } finally { diff --git a/dotnet/test/Unit/JsonRpcTests.cs b/dotnet/test/Unit/JsonRpcTests.cs index 4c82ac0a01..9595d8bd22 100644 --- a/dotnet/test/Unit/JsonRpcTests.cs +++ b/dotnet/test/Unit/JsonRpcTests.cs @@ -102,12 +102,15 @@ public async Task JsonRpc_Cancels_Incoming_Handler_Token_When_Connection_Dispose using var pair = JsonRpcReflectionPair.Create(); var handlerStarted = new TaskCompletionSource( TaskCreationOptions.RunContinuationsAsynchronously); + var handlerDispatched = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); pair.Server.SetLocalRpcMethod("waitForShutdown", (Func)(async cancellationToken => { + await handlerDispatched.Task; handlerStarted.SetResult(cancellationToken); await Task.Delay(Timeout.InfiniteTimeSpan, cancellationToken); })); + pair.Server.SetLocalRpcMethod("handlerDispatched", (Action)(() => handlerDispatched.SetResult())); if (requestHasId) { @@ -117,6 +120,8 @@ public async Task JsonRpc_Cancels_Incoming_Handler_Token_When_Connection_Dispose { await pair.SendClientFrameAsync(CreateNotificationFrame("waitForShutdown", "{}")); } + // Keep the handler suspended until dispatch returns, before testing shutdown cancellation. + await pair.SendClientFrameAsync(CreateNotificationFrame("handlerDispatched", "{}")); var handlerToken = await WaitForCancellationAsync(handlerStarted.Task); Assert.False(handlerToken.IsCancellationRequested); @@ -126,6 +131,24 @@ public async Task JsonRpc_Cancels_Incoming_Handler_Token_When_Connection_Dispose await WaitForCancellationAsync(handlerToken); } + [Fact] + public async Task JsonRpc_Invokes_Task_Returning_Handler_To_Completion() + { + using var pair = JsonRpcReflectionPair.Create(); + pair.StartListening(); + + var observed = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + pair.Server.SetLocalRpcMethod("voidAsync", (Func)(async (name, count, cancellationToken) => + { + await Task.Yield(); + observed.SetResult($"{name}:{count}"); + })); + + await pair.Client.InvokeAsync("voidAsync", ["Mona", 2]).WaitAsync(TimeSpan.FromSeconds(5)); + + Assert.Equal("Mona:2", await observed.Task.WaitAsync(TimeSpan.FromSeconds(5))); + } + [Fact] public async Task JsonRpc_Awaits_Declared_Task_Handler_With_Generic_Runtime_Task() { diff --git a/dotnet/test/Unit/MSBuildTargetsTests.cs b/dotnet/test/Unit/MSBuildTargetsTests.cs index 6a26a95688..0d02767dec 100644 --- a/dotnet/test/Unit/MSBuildTargetsTests.cs +++ b/dotnet/test/Unit/MSBuildTargetsTests.cs @@ -245,6 +245,7 @@ public async Task RuntimePackageAssets_AreFilteredAndCopiedToOutput() sandbox.WriteRuntimeCacheAsset("preloads", "extension_bootstrap.mjs", "preload"); sandbox.WriteRuntimeCacheAsset("sdk", "factory.js", "factory"); sandbox.WriteRuntimeCacheAsset("app.js", "excluded"); + sandbox.WriteRuntimeCacheAsset("cli-main.js", "excluded"); sandbox.WriteRuntimeCacheAsset("LICENSE.md", "excluded"); sandbox.WriteRuntimeCacheAsset("README.md", "excluded"); sandbox.WriteStaleOutputRuntimeAsset("obsolete", "tool", "stale"); @@ -261,6 +262,7 @@ public async Task RuntimePackageAssets_AreFilteredAndCopiedToOutput() Assert.Equal("preload", File.ReadAllText(sandbox.ExpectedRuntimeAsset("preloads", "extension_bootstrap.mjs"))); Assert.Equal("factory", File.ReadAllText(sandbox.ExpectedRuntimeAsset("sdk", "factory.js"))); Assert.False(File.Exists(sandbox.ExpectedRuntimeAsset("app.js"))); + Assert.False(File.Exists(sandbox.ExpectedRuntimeAsset("cli-main.js"))); Assert.False(File.Exists(sandbox.ExpectedRuntimeAsset("LICENSE.md"))); Assert.False(File.Exists(sandbox.ExpectedRuntimeAsset("README.md"))); Assert.False(File.Exists(sandbox.ExpectedRuntimeAsset("obsolete", "tool"))); @@ -274,6 +276,8 @@ public async Task PackAsTool_NoBuild_IncludesRuntimeAssetsInToolPackage() sandbox.WriteRuntimeCacheAsset("prebuilds", GetReleasePlatform(), "runtime.node", "runtime"); sandbox.WriteRuntimeCacheAsset("prebuilds", GetReleasePlatform(), RuntimeWrapperName, "wrapper"); sandbox.WriteRuntimeCacheAsset("ripgrep", "bin", GetReleasePlatform(), "rg", "ripgrep"); + sandbox.WriteRuntimeCacheAsset("app.js", "excluded"); + sandbox.WriteRuntimeCacheAsset("cli-main.js", "excluded"); var result = await sandbox.PackNoBuildAsync(new Dictionary { @@ -290,6 +294,8 @@ public async Task PackAsTool_NoBuild_IncludesRuntimeAssetsInToolPackage() Assert.Contains($"{nativePath}/runtime.node", entries); Assert.Contains($"{nativePath}/{RuntimeLibraryName}", entries); Assert.Contains($"{nativePath}/ripgrep/bin/{GetReleasePlatform()}/rg", entries); + Assert.DoesNotContain($"{nativePath}/app.js", entries); + Assert.DoesNotContain($"{nativePath}/cli-main.js", entries); } [Fact] diff --git a/dotnet/test/Unit/ReplayProxyTests.cs b/dotnet/test/Unit/ReplayProxyTests.cs new file mode 100644 index 0000000000..827d7103d7 --- /dev/null +++ b/dotnet/test/Unit/ReplayProxyTests.cs @@ -0,0 +1,43 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +using System.Text.Json; +using System.Text.Json.Serialization; +using GitHub.Copilot.Test.Harness; +using Xunit; + +namespace GitHub.Copilot.Test.Unit; + +public partial class ReplayProxyTests +{ + [Theory] + [InlineData("37", 37L)] + [InlineData("0", 0L)] + [InlineData("", null)] + [InlineData("null", null)] + public void CapturedCompactionUsagePreservesInputCounts(string inputTokens, long? expectedInputTokens) + { + var inputProperty = inputTokens.Length == 0 ? string.Empty : ",\"inputTokens\":" + inputTokens; + var json = "[{\"request\":{\"model\":\"test-model\",\"messages\":[]}," + + "\"response\":{\"id\":\"compaction-response\",\"model\":\"test-model\"," + + "\"choices\":[]}," + + "\"compactionUsage\":{\"interactionId\":\"compaction-interaction\"," + + "\"summary\":\"summary\",\"responseCount\":2" + inputProperty + "}," + + "\"requestHeaders\":{\"x-interaction-type\":\"conversation-compaction\"}}]"; + var exchanges = JsonSerializer.Deserialize(json, ReplayProxyTestJsonContext.Default.ListParsedHttpExchange); + Assert.NotNull(exchanges); + var exchange = Assert.Single(exchanges); + Assert.NotNull(exchange.RequestHeaders); + Assert.Equal("conversation-compaction", exchange.RequestHeaders["x-interaction-type"].GetString()); + Assert.NotNull(exchange.CompactionUsage); + Assert.Equal("compaction-interaction", exchange.CompactionUsage.InteractionId); + Assert.Equal("summary", exchange.CompactionUsage.Summary); + Assert.Equal(2, exchange.CompactionUsage.ResponseCount); + Assert.Equal(expectedInputTokens, exchange.CompactionUsage.InputTokens); + } + + [JsonSourceGenerationOptions(JsonSerializerDefaults.Web)] + [JsonSerializable(typeof(List))] + private partial class ReplayProxyTestJsonContext : JsonSerializerContext; +} diff --git a/dotnet/test/Unit/SetToolsTests.cs b/dotnet/test/Unit/SetToolsTests.cs index be18c35371..4779a3a70e 100644 --- a/dotnet/test/Unit/SetToolsTests.cs +++ b/dotnet/test/Unit/SetToolsTests.cs @@ -177,23 +177,67 @@ public async Task SetToolsAsync_Cancellation_After_Request_Was_Sent_Still_Instal await using var server = await SetToolsFakeServer.StartAsync(); var releaseSet = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); server.BeforeSetToolsResponseAsync = _ => releaseSet.Task; + using var acceptanceContext = new DeferredSynchronizationContext(); await using var client = new CopilotClient(new CopilotClientOptions { Connection = RuntimeConnection.ForUri(server.Url) }); await using var session = await client.CreateSessionAsync(new SessionConfig { - Tools = [Tool("cancel_tool", "old")], + Tools = [AIFunctionFactory.Create(() => + { + acceptanceContext.Resume(); + return "old"; + }, new AIFunctionFactoryOptions { Name = "cancel_tool", Description = "Returns old" })], }); - using var cts = new CancellationTokenSource(); - var replacement = session.SetToolsAsync([Tool("cancel_tool", "new")], cts.Token); - await server.WaitForRequestAsync("session.tools.set"); - await cts.CancelAsync(); - await Assert.ThrowsAnyAsync(() => replacement); + try + { + using var cts = new CancellationTokenSource(); + var replacement = acceptanceContext.Run(() => session.SetToolsAsync([Tool("cancel_tool", "new")], cts.Token)); + await server.WaitForRequestAsync("session.tools.set"); + await cts.CancelAsync(); + await acceptanceContext.RunNextAsync(); + await Assert.ThrowsAnyAsync(() => replacement); + + releaseSet.SetResult(); + await acceptanceContext.WaitForPendingAsync(); + using var acceptanceTimeout = new CancellationTokenSource(TimeSpan.FromSeconds(5)); + string? result; + try + { + result = await InvokeToolUntilAsync(server, session, "cancel_tool", "cancel-request", "new", acceptanceTimeout.Token); + } + catch (OperationCanceledException ex) when (acceptanceTimeout.IsCancellationRequested) + { + throw new TimeoutException(ex.Message, ex); + } + Assert.Equal("new", result); + } + finally + { + releaseSet.TrySetResult(); + acceptanceContext.Resume(); + } + } - releaseSet.SetResult(); - await server.WaitForSetToolsResponseCountAsync(1); + [Fact] + public async Task InvokeToolUntilAsync_Rejects_Expected_Result_After_Deadline() + { + using var deadline = new CancellationTokenSource(); + await using var server = await SetToolsFakeServer.StartAsync(); + await using var client = new CopilotClient(new CopilotClientOptions { Connection = RuntimeConnection.ForUri(server.Url) }); + await using var session = await client.CreateSessionAsync(new SessionConfig + { + Tools = [AIFunctionFactory.Create(() => + { + deadline.Cancel(); + return "new"; + }, new AIFunctionFactoryOptions { Name = "late_tool", Description = "Returns after the wait deadline" })], + }); + + await Assert.ThrowsAnyAsync(() => + InvokeToolUntilAsync(server, session, "late_tool", "late-request", "new", deadline.Token)); - var result = await InvokeToolAsync(server, session, "cancel_tool", "cancel-request"); - Assert.Equal("new", result); + var response = await server.WaitForRequestAsync("session.tools.handlePendingToolCall", "late-request-0"); + Assert.Equal("new", response.Params.GetProperty("result").GetProperty("textResultForLlm").GetString()); } [Fact] @@ -244,14 +288,69 @@ private static AIFunction Tool(string name, string result) return (AIFunction?)method.Invoke(session, [name]); } - private static async Task InvokeToolAsync(SetToolsFakeServer server, CopilotSession session, string toolName, string requestId) + private static async Task InvokeToolAsync( + SetToolsFakeServer server, + CopilotSession session, + string toolName, + string requestId, + CancellationToken cancellationToken = default) { - await SendToolRequestAsync(server, session, toolName, requestId); - var request = await server.WaitForRequestAsync("session.tools.handlePendingToolCall", requestId); + await SendToolRequestAsync(server, session, toolName, requestId, cancellationToken); + var request = await server.WaitForRequestAsync("session.tools.handlePendingToolCall", requestId, cancellationToken); return request.Params.GetProperty("result").GetProperty("textResultForLlm").GetString(); } - private static Task SendToolRequestAsync(SetToolsFakeServer server, CopilotSession session, string toolName, string requestId) + /// + /// Invokes until the replacement handler answers it, or a timeout expires. + /// + /// + /// Writing a session.tools.set response happens before the client has read it, completed the RPC, + /// and published the replacement handlers. Tests that + /// await a successful SetToolsAsync task complete after that publish and need no polling; the + /// cancellation test cannot, because the caller's await threw and the replacement task runs detached. + /// Polling does not weaken the assertion: a replacement that never installs still fails, on the timeout. + /// + private static async Task InvokeToolUntilAsync( + SetToolsFakeServer server, + CopilotSession session, + string toolName, + string requestId, + string expected, + CancellationToken cancellationToken) + { + var attempt = 0; + string? result = null; + try + { + while (true) + { + // Each invocation needs its own id: the server records one pending call per request id. + result = await InvokeToolAsync(server, session, toolName, $"{requestId}-{attempt++}", cancellationToken); + cancellationToken.ThrowIfCancellationRequested(); + if (result == expected) + { + return result; + } + + Assert.Equal("old", result); + await Task.Delay(20, cancellationToken); + } + } + catch (OperationCanceledException ex) when (cancellationToken.IsCancellationRequested) + { + throw new OperationCanceledException( + $"Accepted replacement did not install '{expected}'; last tool result was '{result ?? ""}'.", + ex, + cancellationToken); + } + } + + private static Task SendToolRequestAsync( + SetToolsFakeServer server, + CopilotSession session, + string toolName, + string requestId, + CancellationToken cancellationToken = default) { using var arguments = JsonDocument.Parse("{}"); return server.SendSessionEventAsync(session.SessionId, "external_tool.requested", new Dictionary @@ -261,11 +360,87 @@ private static Task SendToolRequestAsync(SetToolsFakeServer server, CopilotSessi ["toolCallId"] = requestId + "-call", ["toolName"] = toolName, ["arguments"] = arguments.RootElement.Clone(), - }); + }, cancellationToken); } private sealed record RpcRequestRecord(string Method, JsonElement Params); + private sealed class DeferredSynchronizationContext : SynchronizationContext, IDisposable + { + private readonly object _gate = new(); + private readonly Queue<(SendOrPostCallback Callback, object? State)> _callbacks = new(); + private readonly SemaphoreSlim _available = new(0); + private bool _resumed; + + public Task Run(Func action) + { + var previous = Current; + SetSynchronizationContext(this); + try + { + return action(); + } + finally + { + SetSynchronizationContext(previous); + } + } + + public override void Post(SendOrPostCallback callback, object? state) + { + lock (_gate) + { + if (!_resumed) + { + _callbacks.Enqueue((callback, state)); + _available.Release(); + return; + } + } + ThreadPool.QueueUserWorkItem(_ => callback(state)); + } + + public async Task WaitForPendingAsync() + { + if (!await _available.WaitAsync(TimeSpan.FromSeconds(5))) + { + throw new TimeoutException("The client did not post its tool-replacement continuation."); + } + } + + public async Task RunNextAsync() + { + await WaitForPendingAsync(); + (SendOrPostCallback Callback, object? State) next; + lock (_gate) + { + next = _callbacks.Dequeue(); + } + next.Callback(next.State); + } + + public void Resume() + { + (SendOrPostCallback Callback, object? State)[] pending; + lock (_gate) + { + _resumed = true; + pending = _callbacks.ToArray(); + _callbacks.Clear(); + } + foreach (var (callback, state) in pending) + { + ThreadPool.QueueUserWorkItem(_ => callback(state)); + } + } + + public void Dispose() + { + Resume(); + _available.Dispose(); + } + } + private sealed class SetToolsFakeServer : IAsyncDisposable { private static readonly JsonSerializerOptions s_jsonOptions = new(JsonSerializerDefaults.Web) @@ -282,7 +457,6 @@ private sealed class SetToolsFakeServer : IAsyncDisposable private string? _nextSetToolsRejection; private readonly Dictionary _setToolsRejections = []; private int _setToolsCalls; - private int _setToolsResponses; private SetToolsFakeServer(TcpListener listener) { @@ -339,11 +513,16 @@ public void RejectSetToolsCall(int callNumber, string message) _setToolsRejections[callNumber] = message; } - public async Task WaitForRequestAsync(string method, string? requestId = null) + public async Task WaitForRequestAsync( + string method, + string? requestId = null, + CancellationToken cancellationToken = default) { - using var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(5)); + using var timeout = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); + timeout.CancelAfter(TimeSpan.FromSeconds(5)); while (!timeout.IsCancellationRequested) { + cancellationToken.ThrowIfCancellationRequested(); var request = Requests.FirstOrDefault(request => request.Method == method && (requestId is null || request.Params.GetProperty("requestId").GetString() == requestId)); @@ -352,30 +531,20 @@ public async Task WaitForRequestAsync(string method, string? r return request; } - await Task.Delay(20, CancellationToken.None); + await Task.Delay(20, cancellationToken); } + cancellationToken.ThrowIfCancellationRequested(); throw new TimeoutException($"Timed out waiting for RPC method '{method}'."); } - public async Task WaitForSetToolsResponseCountAsync(int count) - { - using var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(5)); - while (!timeout.IsCancellationRequested) - { - if (Volatile.Read(ref _setToolsResponses) >= count) - { - return; - } - - await Task.Delay(20, CancellationToken.None); - } - - throw new TimeoutException($"Timed out waiting for {count} session.tools.set response(s)."); - } - - public Task SendSessionEventAsync(string sessionId, string type, Dictionary data) + public async Task SendSessionEventAsync( + string sessionId, + string type, + Dictionary data, + CancellationToken cancellationToken = default) { + using var linkedCancellation = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, _cts.Token); var stream = _stream ?? throw new InvalidOperationException("Client is not connected."); var evt = new Dictionary { @@ -385,7 +554,7 @@ public Task SendSessionEventAsync(string sessionId, string type, Dictionary + await WriteMessageAsync(stream, new Dictionary { ["jsonrpc"] = "2.0", ["method"] = "session.event", @@ -394,7 +563,7 @@ public Task SendSessionEventAsync(string sessionId, string type, Dictionary= 2 { + if len(snapshot) != 2 || snapshot[0].Method != "session.create" || snapshot[1].Method != "session.delete" { + t.Fatalf("expected one create and one orphan cleanup, got %+v", snapshot) + } + if snapshot[1].Params["sessionId"] != "server-assigned-session-1" { + t.Fatalf("expected cleanup of server-assigned session, got %+v", snapshot[1]) + } + return + } + select { + case <-requests.notify: + case <-deadline.C: + t.Fatalf("cloud session was not deleted after provider validation failed; requests: %+v", snapshot) + } + } +} + func TestClient_AuthOptions(t *testing.T) { t.Run("should accept GitHubToken option", func(t *testing.T) { client := NewClient(&ClientOptions{ @@ -3138,12 +3374,19 @@ type recordedRequest struct { type requestRecorder struct { mu sync.Mutex requests []recordedRequest + notify chan struct{} } func (r *requestRecorder) append(request recordedRequest) { r.mu.Lock() defer r.mu.Unlock() r.requests = append(r.requests, request) + if r.notify != nil { + select { + case r.notify <- struct{}{}: + default: + } + } } func (r *requestRecorder) snapshot() []recordedRequest { @@ -3232,6 +3475,8 @@ func serveInMemoryRuntime(t *testing.T, stdinR *io.PipeReader, stdoutW *io.PipeW result = map[string]any{} case "session.detach": result = map[string]any{"success": true} + case "session.delete": + result = map[string]any{"success": true} default: t.Errorf("unexpected JSON-RPC method %s", request.Method) return diff --git a/go/cmd/bundler/main.go b/go/cmd/bundler/main.go index 8c088eb488..f7a34413d1 100644 --- a/go/cmd/bundler/main.go +++ b/go/cmd/bundler/main.go @@ -541,7 +541,7 @@ func runtimeWrapperName(binaryName string) string { } var hostlessExcludedTopLevel = map[string]bool{ - "app.js": true, "assets": true, "changelog.json": true, "copilot": true, "copilot.exe": true, + "app.js": true, "assets": true, "changelog.json": true, "cli-main.js": true, "copilot": true, "copilot.exe": true, "foundry-local-sdk": true, "index.js": true, "napi-oop-runtime": true, "LICENSE.md": true, "npm-loader.js": true, "package.json": true, "pvrecorder": true, "queries": true, "README.md": true, "sea-loader.js": true, "webview": true, diff --git a/go/cmd/bundler/main_test.go b/go/cmd/bundler/main_test.go index 7325489767..90bdea1689 100644 --- a/go/cmd/bundler/main_test.go +++ b/go/cmd/bundler/main_test.go @@ -30,6 +30,7 @@ func TestCreateRuntimeAssetsArchiveRetainsUnknownAssetsAndFiltersCLIContent(t *t "package/preloads/extension_bootstrap.mjs": "preload", "package/sdk/factory.js": "factory", "package/app.js": "excluded", + "package/cli-main.js": "excluded", "package/LICENSE.md": "excluded", "package/README.md": "excluded", }) @@ -50,7 +51,7 @@ func TestCreateRuntimeAssetsArchiveRetainsUnknownAssetsAndFiltersCLIContent(t *t t.Fatalf("retained assets = %#v", files) } for _, excluded := range []string{ - "runtime.node", "copilot-runtime", "app.js", "LICENSE.md", "README.md", + "runtime.node", "copilot-runtime", "app.js", "cli-main.js", "LICENSE.md", "README.md", } { if _, ok := files[excluded]; ok { t.Fatalf("excluded asset %q was retained", excluded) diff --git a/go/internal/e2e/compaction_e2e_test.go b/go/internal/e2e/compaction_e2e_test.go index e994cdfc7e..9bf2084e52 100644 --- a/go/internal/e2e/compaction_e2e_test.go +++ b/go/internal/e2e/compaction_e2e_test.go @@ -2,6 +2,7 @@ package e2e import ( "errors" + "os" "strings" "testing" "time" @@ -118,8 +119,40 @@ func TestCompactionE2E(t *testing.T) { } if completeData.CompactionTokensUsed == nil { t.Errorf("Expected compaction tokens-used data") - } else if completeData.CompactionTokensUsed.InputTokens == nil || *completeData.CompactionTokensUsed.InputTokens <= 0 { - t.Errorf("Expected compaction call to consume input tokens, got %v", completeData.CompactionTokensUsed.InputTokens) + } else { + actualInputTokens := completeData.CompactionTokensUsed.InputTokens + // Replay has no prompt usage; recording must preserve the provider's value or absence. + if os.Getenv("GITHUB_ACTIONS") == "true" { + if actualInputTokens != nil && *actualInputTokens != 0 { + t.Errorf("Expected replay compaction input tokens to be zero or unavailable, got %d", *actualInputTokens) + } + } else { + var expectedInputTokens *int64 + exchanges, err := ctx.GetExchanges() + if err != nil { + t.Fatalf("Failed to get provider responses: %v", err) + } + var compactions []*testharness.CompactionProviderUsage + for _, exchange := range exchanges { + if exchange.CompactionUsage != nil && + completeData.SummaryContent != nil && exchange.CompactionUsage.Summary == *completeData.SummaryContent { + compactions = append(compactions, exchange.CompactionUsage) + } + } + if len(compactions) != 1 { + t.Fatalf("Expected one provider compaction chain for the completed summary, got %d", len(compactions)) + } + expectedInputTokens = compactions[0].InputTokens + if expectedInputTokens == nil { + if actualInputTokens != nil { + t.Errorf("Expected provider input tokens to be unavailable, got %d", *actualInputTokens) + } + } else if actualInputTokens == nil { + t.Errorf("Expected provider input tokens %d, got unavailable", *expectedInputTokens) + } else if *actualInputTokens != *expectedInputTokens { + t.Errorf("Expected provider input tokens %d, got %d", *expectedInputTokens, *actualInputTokens) + } + } } summary := "" if completeData.SummaryContent != nil { diff --git a/go/internal/e2e/hooks_extended_e2e_test.go b/go/internal/e2e/hooks_extended_e2e_test.go index 5cbba38566..2c804eba99 100644 --- a/go/internal/e2e/hooks_extended_e2e_test.go +++ b/go/internal/e2e/hooks_extended_e2e_test.go @@ -13,13 +13,13 @@ import ( // Mirrors dotnet/test/HookLifecycleAndOutputTests.cs (snapshot category "hooks_extended"). // -// Covers each handler exposed on copilot.SessionHooks: OnPreToolUse, +// Covers the general-purpose handlers on copilot.SessionHooks: OnPreToolUse, // OnPostToolUse, OnPostToolUseFailure, OnUserPromptSubmitted, // OnUserPromptTransformed, OnSessionStart, OnSessionEnd, OnErrorOccurred, -// OnAgentStop. Output-shape behavior (modifiedPrompt / modifiedTransformedPrompt / +// OnAgentStop. Subagent hooks are exercised in subagent_hooks_e2e_test.go. +// Output-shape behavior (modifiedPrompt / modifiedTransformedPrompt / // additionalContext / errorHandling / modifiedArgs / modifiedResult / -// sessionSummary) is asserted alongside hook invocation. If a new handler is -// added to SessionHooks, add a corresponding test here. +// sessionSummary) is asserted alongside hook invocation. func TestHooksExtendedE2E(t *testing.T) { ctx := testharness.NewTestContext(t) client := ctx.NewClient() diff --git a/go/internal/e2e/session_fs_e2e_test.go b/go/internal/e2e/session_fs_e2e_test.go index 4236e8e529..18d41a46b5 100644 --- a/go/internal/e2e/session_fs_e2e_test.go +++ b/go/internal/e2e/session_fs_e2e_test.go @@ -1,6 +1,7 @@ package e2e import ( + "encoding/base64" "fmt" "os" "path/filepath" @@ -76,6 +77,84 @@ func TestSessionFSE2E(t *testing.T) { } }) + t.Run("should view an image that exists only in the binary session fs provider", func(t *testing.T) { + ctx.ConfigureForTest(t) + + imagePath := "/sdk-provider-image.png" + imageBytes, err := base64.StdEncoding.DecodeString("iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8DwHwAFBQIAX8jx0gAAAABJRU5ErkJggg==") + if err != nil { + t.Fatal(err) + } + binaryConfig := *sessionFSConfig + binaryConfig.Capabilities = &copilot.SessionFSCapabilities{Binary: true} + binaryClient := ctx.NewClient(func(opts *copilot.ClientOptions) { + opts.SessionFS = &binaryConfig + }) + t.Cleanup(func() { binaryClient.ForceStop() }) + readPaths := make(chan string, 1) + session, err := binaryClient.CreateSession(t.Context(), &copilot.SessionConfig{ + OnPermissionRequest: copilot.PermissionHandler.ApproveAll, + CreateSessionFSProvider: func(session *copilot.Session) copilot.SessionFSProvider { + return &testBinarySessionFSHandler{ + testSessionFSHandler: &testSessionFSHandler{root: providerRoot, sessionID: session.SessionID}, + readPaths: readPaths, + } + }, + }) + if err != nil { + t.Fatalf("Failed to create binary session: %v", err) + } + storedPath := p(session.SessionID, imagePath) + if err := os.MkdirAll(filepath.Dir(storedPath), 0o755); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(storedPath, imageBytes, 0o644); err != nil { + t.Fatal(err) + } + if _, err := os.Stat(imagePath); !os.IsNotExist(err) { + t.Fatalf("Image unexpectedly exists on the runtime filesystem: %v", err) + } + + msg, err := session.SendAndWait(t.Context(), copilot.MessageOptions{ + Prompt: "Use the view tool to view /sdk-provider-image.png, then reply with exactly SDK_PROVIDER_IMAGE_DONE.", + }) + if err != nil { + t.Fatalf("Failed to view provider image: %v", err) + } + if msg == nil { + t.Fatal("Expected a final assistant response") + } + response, ok := msg.Data.(*copilot.AssistantMessageData) + if !ok || !strings.Contains(response.Content, "SDK_PROVIDER_IMAGE_DONE") { + t.Fatalf("Expected final image response, got %#v", msg) + } + select { + case path := <-readPaths: + if path != imagePath { + t.Fatalf("Expected binary read of %q, got %q", imagePath, path) + } + default: + t.Fatal("Expected a binary read from the provider") + } + events, err := session.GetEvents(t.Context()) + if err != nil { + t.Fatalf("Failed to read session events: %v", err) + } + foundAsset := false + for _, event := range events { + if asset, ok := event.Data.(*copilot.SessionBinaryAssetData); ok && + asset.MIMEType == "image/png" && asset.Data == base64.StdEncoding.EncodeToString(imageBytes) { + foundAsset = true + } + } + if !foundAsset { + t.Fatal("Expected a binary asset with the exact provider image bytes") + } + if err := session.Disconnect(); err != nil { + t.Fatalf("Failed to disconnect session: %v", err) + } + }) + t.Run("should load session data from fs provider on resume", func(t *testing.T) { ctx.ConfigureForTest(t) @@ -346,6 +425,31 @@ type testSessionFSHandler struct { sessionID string } +type testBinarySessionFSHandler struct { + *testSessionFSHandler + readPaths chan<- string +} + +func (h *testBinarySessionFSHandler) ReadFileBytes(path string) ([]byte, error) { + select { + case h.readPaths <- path: + default: + } + return os.ReadFile(providerPath(h.root, h.sessionID, path)) +} + +func (h *testBinarySessionFSHandler) WriteFileBytes(path string, content []byte, mode *int) error { + fullPath := providerPath(h.root, h.sessionID, path) + if err := os.MkdirAll(filepath.Dir(fullPath), 0o755); err != nil { + return err + } + perm := os.FileMode(0o666) + if mode != nil { + perm = os.FileMode(*mode) + } + return os.WriteFile(fullPath, content, perm) +} + func (h *testSessionFSHandler) ReadFile(path string) (string, error) { content, err := os.ReadFile(providerPath(h.root, h.sessionID, path)) if err != nil { diff --git a/go/internal/e2e/subagent_hooks_e2e_test.go b/go/internal/e2e/subagent_hooks_e2e_test.go index 2f2cedf904..f74cb6355d 100644 --- a/go/internal/e2e/subagent_hooks_e2e_test.go +++ b/go/internal/e2e/subagent_hooks_e2e_test.go @@ -1,37 +1,52 @@ package e2e import ( + "bytes" + "encoding/json" + "io" "net/http" "os" "path/filepath" + "strings" "sync" "testing" + "time" copilot "github.com/github/copilot-sdk/go" "github.com/github/copilot-sdk/go/internal/e2e/testharness" "github.com/google/uuid" ) +const childContext = "Subagent start hook verified: read the requested file." +const stopResponsePrefix = "Subagent stop hook verified: " + type subagentRequestRecord struct { agentID string parentAgentID string interactionType string + childPrompt string } type recordingForwardingTransport struct { - inner http.RoundTripper - mu sync.Mutex - records []subagentRequestRecord + inner http.RoundTripper + mu sync.Mutex + records []subagentRequestRecord + modifiedParentRequest chan struct{} } func newRecordingForwardingTransport() *recordingForwardingTransport { inner := http.DefaultTransport.(*http.Transport).Clone() inner.DisableCompression = true - return &recordingForwardingTransport{inner: inner} + return &recordingForwardingTransport{inner: inner, modifiedParentRequest: make(chan struct{}, 1)} } func (rt *recordingForwardingTransport) RoundTrip(req *http.Request) (*http.Response, error) { if isInferenceURL(req.URL.String()) { + body, err := io.ReadAll(req.Body) + if err != nil { + return nil, err + } + req.Body = io.NopCloser(bytes.NewReader(body)) rctx := copilot.RequestContextFrom(req) record := subagentRequestRecord{} if rctx != nil { @@ -39,6 +54,50 @@ func (rt *recordingForwardingTransport) RoundTrip(req *http.Request) (*http.Resp record.parentAgentID = rctx.ParentAgentID record.interactionType = rctx.InteractionType } + if record.parentAgentID != "" { + var payload struct { + Messages []struct { + Role string `json:"role"` + Content json.RawMessage `json:"content"` + } `json:"messages"` + } + if err := json.Unmarshal(body, &payload); err != nil { + return nil, err + } + for _, message := range payload.Messages { + if message.Role == "user" && len(message.Content) > 0 { + var prompts []string + switch message.Content[0] { + case '"': + var prompt string + if err := json.Unmarshal(message.Content, &prompt); err != nil { + return nil, err + } + prompts = append(prompts, prompt) + case '[': + var parts []struct { + Text string `json:"text"` + } + if err := json.Unmarshal(message.Content, &parts); err != nil { + return nil, err + } + for _, part := range parts { + prompts = append(prompts, part.Text) + } + } + for _, prompt := range prompts { + if strings.Contains(prompt, childContext+"\n\n") { + record.childPrompt = prompt + } + } + } + } + } else if bytes.Contains(body, []byte(stopResponsePrefix)) { + select { + case rt.modifiedParentRequest <- struct{}{}: + default: + } + } rt.mu.Lock() rt.records = append(rt.records, record) rt.mu.Unlock() @@ -87,7 +146,7 @@ func TestSubagentHooksE2E(t *testing.T) { }) t.Cleanup(func() { client.ForceStop() }) - t.Run("should invoke preToolUse and postToolUse hooks for sub-agent tool calls", func(t *testing.T) { + t.Run("should apply subagent lifecycle hook outputs", func(t *testing.T) { ctx.ConfigureForTest(t) type hookEntry struct { @@ -103,6 +162,11 @@ func TestSubagentHooksE2E(t *testing.T) { parentWaiting := make(chan struct{}) releaseView := sync.OnceFunc(func() { close(parentWaiting) }) defer releaseView() + var startInputs []copilot.SubagentStartHookInput + var stopInputs []copilot.SubagentStopHookInput + var startInvocations []copilot.HookInvocation + var stopInvocations []copilot.HookInvocation + stopObserved := make(chan struct{}, 1) session, err := client.CreateSession(t.Context(), &copilot.SessionConfig{ SessionID: parentSessionID, @@ -128,12 +192,29 @@ func TestSubagentHooksE2E(t *testing.T) { } return nil, nil }, + OnSubagentStart: func(input copilot.SubagentStartHookInput, invocation copilot.HookInvocation) (*copilot.SubagentStartHookOutput, error) { + mu.Lock() + startInputs = append(startInputs, input) + startInvocations = append(startInvocations, invocation) + mu.Unlock() + return &copilot.SubagentStartHookOutput{AdditionalContext: childContext}, nil + }, + OnSubagentStop: func(input copilot.SubagentStopHookInput, invocation copilot.HookInvocation) (*copilot.SubagentStopHookOutput, error) { + mu.Lock() + stopInputs = append(stopInputs, input) + stopInvocations = append(stopInvocations, invocation) + mu.Unlock() + select { + case stopObserved <- struct{}{}: + default: + } + return &copilot.SubagentStopHookOutput{ModifiedResponse: copilot.String(stopResponsePrefix + input.Response)}, nil + }, }, }) if err != nil { t.Fatalf("Failed to create session: %v", err) } - // Create a file for the sub-agent to read testFile := filepath.Join(ctx.WorkDir, "subagent-test.txt") if err := os.WriteFile(testFile, []byte("Hello from subagent test!"), 0644); err != nil { @@ -173,8 +254,57 @@ func TestSubagentHooksE2E(t *testing.T) { t.Fatalf("Expected durable waiting reply followed by final reply, got %q", replies) } + select { + case <-stopObserved: + case <-time.After(120 * time.Second): + t.Fatal("Timed out waiting for the subagentStop hook") + } + select { + case <-transport.modifiedParentRequest: + case <-time.After(120 * time.Second): + t.Fatal("Timed out waiting for the parent inference request with the rewritten response") + } mu.Lock() defer mu.Unlock() + if len(startInputs) != 1 || len(stopInputs) != 1 { + t.Fatalf("Expected one subagentStart and one subagentStop invocation, got starts=%+v stops=%+v", startInputs, stopInputs) + } + start, stop := startInputs[0], stopInputs[0] + if startInvocations[0].SessionID != session.SessionID || stopInvocations[0].SessionID != session.SessionID || + start.SessionID != session.SessionID || stop.SessionID != session.SessionID { + t.Errorf("Expected parent session ID %q in hook inputs and invocations, got start=%+v stop=%+v", session.SessionID, start, stop) + } + if !start.Timestamp.After(time.UnixMilli(0)) || !stop.Timestamp.After(time.UnixMilli(0)) || + stop.Timestamp.Before(start.Timestamp) { + t.Errorf("Expected ordered timestamps, got start=%v stop=%v", start.Timestamp, stop.Timestamp) + } + if start.WorkingDirectory != ctx.WorkDir || stop.WorkingDirectory != ctx.WorkDir { + t.Errorf("Expected working directory %q, got start=%q stop=%q", ctx.WorkDir, start.WorkingDirectory, stop.WorkingDirectory) + } + if stop.TranscriptPath != start.TranscriptPath || (start.TranscriptPath != "" && !filepath.IsAbs(start.TranscriptPath)) { + t.Errorf("Expected matching transcript paths (absolute when available), got start=%q stop=%q", start.TranscriptPath, stop.TranscriptPath) + } + if start.AgentName != "explore" || stop.AgentName != start.AgentName || stop.AgentType != "explore" { + t.Errorf("Expected explore agent name and type, got start=%+v stop=%+v", start, stop) + } + if start.AgentDisplayName != "" { + t.Errorf("Expected no optional display name on this task-created agent, got %q", start.AgentDisplayName) + } + if stop.AgentDisplayName != start.AgentDisplayName { + t.Errorf("Expected stop display name %q, got %q", start.AgentDisplayName, stop.AgentDisplayName) + } + if start.AgentDescription != "" { + t.Errorf("Expected no optional description on this task-created agent, got %q", start.AgentDescription) + } + if stop.AgentDescription != start.AgentDescription { + t.Errorf("Expected stop description %q, got %q", start.AgentDescription, stop.AgentDescription) + } + if stop.AgentID == "" { + t.Error("Expected a runtime-generated sub-agent ID") + } + if stop.StopReason != "end_turn" || !strings.Contains(stop.Response, "Hello from subagent test!") { + t.Errorf("Expected completed turn and file contents in stop input, got %+v", stop) + } // Parent tool hooks fire for "task" var taskPre *hookEntry @@ -211,6 +341,16 @@ func TestSubagentHooksE2E(t *testing.T) { if viewPre[0].sessionID == taskPre.sessionID { t.Error("Sub-agent tool hooks should have a different sessionId than parent tool hooks") } - assertSubagentRequestMetadata(t, transport.inferenceRecords()) + requests := transport.inferenceRecords() + assertSubagentRequestMetadata(t, requests) + startContextObserved := false + for _, request := range requests { + if strings.Contains(request.childPrompt, childContext+"\n\nRead the file \"subagent-test.txt\"") { + startContextObserved = true + } + } + if !startContextObserved { + t.Errorf("Start hook context did not reach a child inference prompt: %+v", requests) + } }) } diff --git a/go/internal/e2e/testharness/proxy.go b/go/internal/e2e/testharness/proxy.go index 077ec45b6d..488215f8a6 100644 --- a/go/internal/e2e/testharness/proxy.go +++ b/go/internal/e2e/testharness/proxy.go @@ -281,9 +281,18 @@ type CapturedRequest struct { // ParsedHttpExchange represents a captured HTTP exchange. type ParsedHttpExchange struct { - Request ChatCompletionRequest `json:"request"` - Response *ChatCompletionResponse `json:"response,omitempty"` - RequestHeaders map[string]json.RawMessage `json:"requestHeaders,omitempty"` + Request ChatCompletionRequest `json:"request"` + Response *ChatCompletionResponse `json:"response,omitempty"` + RequestHeaders map[string]json.RawMessage `json:"requestHeaders,omitempty"` + CompactionUsage *CompactionProviderUsage `json:"compactionUsage,omitempty"` +} + +// CompactionProviderUsage folds all responses in one correlated compaction request chain. +type CompactionProviderUsage struct { + InteractionID string `json:"interactionId"` + Summary string `json:"summary"` + ResponseCount int `json:"responseCount"` + InputTokens *int64 `json:"inputTokens,omitempty"` } // ChatCompletionRequest represents an OpenAI chat completion request. diff --git a/go/internal/e2e/testharness/proxy_test.go b/go/internal/e2e/testharness/proxy_test.go new file mode 100644 index 0000000000..cbd79c5548 --- /dev/null +++ b/go/internal/e2e/testharness/proxy_test.go @@ -0,0 +1,53 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. + +package testharness + +import ( + "encoding/json" + "testing" +) + +func TestCompactionProviderUsageInputCounts(t *testing.T) { + for _, tc := range []struct { + name string + inputProperty string + want int64 + known bool + }{ + {"recorded", `,"inputTokens":37`, 37, true}, + {"replayed", `,"inputTokens":0`, 0, true}, + {"unavailable", ``, 0, false}, + {"input null", `,"inputTokens":null`, 0, false}, + } { + t.Run(tc.name, func(t *testing.T) { + var exchange ParsedHttpExchange + body := `{"compactionUsage":{"interactionId":"compaction-interaction","summary":"summary","responseCount":2` + + tc.inputProperty + `},"requestHeaders":{"x-interaction-type":"conversation-compaction"}}` + if err := json.Unmarshal([]byte(body), &exchange); err != nil { + t.Fatal(err) + } + if exchange.CompactionUsage == nil { + t.Fatal("Expected compaction provider usage") + } + var interactionType string + if err := json.Unmarshal(exchange.RequestHeaders["x-interaction-type"], &interactionType); err != nil || interactionType != "conversation-compaction" { + t.Fatalf("Compaction interaction header = %q, %v", interactionType, err) + } + usage := exchange.CompactionUsage + if usage.InteractionID != "compaction-interaction" || usage.Summary != "summary" || usage.ResponseCount != 2 { + t.Fatalf("Unexpected compaction chain metadata: %+v", usage) + } + var got int64 + known := usage.InputTokens != nil + if known != tc.known { + t.Fatalf("Provider input usage availability = %v, want %v", known, tc.known) + } + if known { + got = *usage.InputTokens + } + if got != tc.want { + t.Fatalf("Provider input tokens = %d, want %d", got, tc.want) + } + }) + } +} diff --git a/go/internal/e2e/tools_e2e_test.go b/go/internal/e2e/tools_e2e_test.go index 75613bebdf..a5109b154b 100644 --- a/go/internal/e2e/tools_e2e_test.go +++ b/go/internal/e2e/tools_e2e_test.go @@ -1,3 +1,5 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. + package e2e import ( @@ -18,6 +20,53 @@ func TestToolsE2E(t *testing.T) { client := ctx.NewClient() t.Cleanup(func() { client.ForceStop() }) + t.Run("string schema apply_patch override binds patch input", func(t *testing.T) { + ctx.ConfigureForTest(t) + const patch = "*** Begin Patch\n*** Add File: override-marker.txt\n+from-native\n*** End Patch" + type capturedPatch struct { + patch string + arguments any + } + calls := make(chan capturedPatch, 1) + tool := copilot.DefineTool("apply_patch", "Apply a patch", + func(input string, invocation copilot.ToolInvocation) (string, error) { + calls <- capturedPatch{input, invocation.Arguments} + return "HOST_PATCH_HANDLED", nil + }) + tool.OverridesBuiltInTool = true + session, err := client.CreateSession(t.Context(), &copilot.SessionConfig{ + OnPermissionRequest: copilot.PermissionHandler.ApproveAll, + Tools: []copilot.Tool{tool}, + }) + if err != nil { + t.Fatalf("Failed to create session: %v", err) + } + finalMessage := testharness.SubscribeToFinalAssistantMessage(session) + defer finalMessage.Close() + _, err = session.Send(t.Context(), copilot.MessageOptions{Prompt: "Use apply_patch to apply the supplied patch."}) + if err != nil { + t.Fatalf("Failed to send message: %v", err) + } + answer, err := finalMessage.Wait(t.Context()) + if err != nil { + t.Fatalf("Failed to get assistant message: %v", err) + } + select { + case call := <-calls: + if call.patch != patch || call.arguments != patch { + t.Fatalf("Expected scalar patch %q, got %+v", patch, call) + } + default: + t.Fatal("String-schema handler was not invoked") + } + if message, ok := answer.Data.(*copilot.AssistantMessageData); !ok || message.Content != "Host override completed." { + t.Fatalf("Unexpected final response: %v", answer.Data) + } + if _, err := os.Stat(filepath.Join(ctx.WorkDir, "override-marker.txt")); !os.IsNotExist(err) { + t.Fatalf("Native apply_patch must not create the file: %v", err) + } + }) + t.Run("invokes built-in tools", func(t *testing.T) { ctx.ConfigureForTest(t) diff --git a/go/rpc/zrpc.go b/go/rpc/zrpc.go index 17d1579d4d..c0af9e58ab 100644 --- a/go/rpc/zrpc.go +++ b/go/rpc/zrpc.go @@ -3630,6 +3630,10 @@ type CurrentModel struct { // Latest unclaimed Auto preference waiting for a future user turn. Null means the pending // request is returning to provider-default routing. PendingAutoTier *AutoTier `json:"pendingAutoTier,omitempty"` + // Captured base model to restore when leaving plan mode. Omitted outside plan mode or when + // no plan override has captured a base model. Persistent agent model requirements apply to + // this model rather than the temporary plan model. + PlanBaseModelID *string `json:"planBaseModelId,omitempty"` // Reasoning effort level currently applied to the active model, when one is set. Reads // `Session.getReasoningEffort()` synchronously after `getSelectedModel()` resolves so the // two values are reported as a snapshot. @@ -3656,6 +3660,33 @@ type CurrentToolMetadata struct { NamespacedName *string `json:"namespacedName,omitempty"` } +// Result of one customization reload component. +// Experimental: CustomizationReloadOutcome is part of an experimental API and may change or +// be removed. +type CustomizationReloadOutcome struct { + // Reason for a skipped component or description of a failure, when available + Detail *string `json:"detail,omitempty"` + // Whether the component reloaded, was skipped, or failed + Status CustomizationReloadStatus `json:"status"` + // Component whose reload was attempted or skipped + Subsystem CustomizationReloadSubsystem `json:"subsystem"` +} + +// Results of reloading discovered session customizations. Inspect outcomes for reloaded, +// skipped, or failed subsystems; a rejection may follow partial mutation. Changes to the +// model-facing prompt and tools apply on the next turn. +// Experimental: CustomizationsReloadResult is part of an experimental API and may change or +// be removed. +type CustomizationsReloadResult struct { + // Errors from any component that could not be refreshed + Errors []string `json:"errors"` + // Outcome of each component in reload order; a skipped component was not configured or + // loaded + Outcomes []CustomizationReloadOutcome `json:"outcomes"` + // Warnings from skill discovery + Warnings []string `json:"warnings"` +} + // A file included in the session debug bundle. // Experimental: DebugCollectLogsCollectedEntry is part of an experimental API and may // change or be removed. @@ -3997,6 +4028,44 @@ type DiscoveredMCPServer struct { Type *DiscoveredMCPServerType `json:"type,omitempty"` } +// A model offered for agent conversations. Missing capability metadata does not disqualify +// a candidate. Models known to be incompatible, such as embedding-only models, are excluded +// by the adapter. +// Experimental: DiscoveredModel is part of an experimental API and may change or be removed. +type DiscoveredModel struct { + // Provider-reported model capabilities. Omitted capability fields are unknown; explicit + // false values are preserved. + Capabilities ModelCapabilities `json:"capabilities"` + // Provider-reported model artifact details. + Details ModelArtifactDetails `json:"details"` + // Provider-reported artifact digest. + Digest *string `json:"digest,omitempty"` + // Provider-native model identifier. + ID string `json:"id"` + // Provider-reported last-modified timestamp. + ModifiedAt *time.Time `json:"modifiedAt,omitempty"` + // Provider-reported display name. + Name *string `json:"name,omitempty"` + // Attribution for the adapter that produced this model row. + Provenance ModelProviderProvenance `json:"provenance"` + // Provider-reported artifact size in bytes. + SizeBytes *int64 `json:"sizeBytes,omitempty"` + // Non-fatal warnings encountered while enriching this model. + Warnings []ModelProviderWarning `json:"warnings"` +} + +// Models offered for agent conversations by one provider instance. Adapters exclude +// known-incompatible models, but retain candidates with unknown capabilities. Listing does +// not guarantee compatibility. +// Experimental: DiscoveredModelList is part of an experimental API and may change or be +// removed. +type DiscoveredModelList struct { + // Provider-native models in provider order. + Models []DiscoveredModel `json:"models"` + // Typed operation outcome. + Outcome ModelProviderOperationOutcome `json:"outcome"` +} + // Slash-prefixed command string to enqueue for FIFO processing. // Experimental: EnqueueCommandParams is part of an experimental API and may change or be // removed. @@ -4040,66 +4109,6 @@ func (UnsupportedEnqueueCommandResult) Queued() bool { return false } -// OneAuth token request supplied by a trusted host application. -// Experimental: EntraTokenAcquireRequest is part of an experimental API and may change or -// be removed. -type EntraTokenAcquireRequest struct { - // Previously rejected token that OneAuth must bypass during renewal. - AccessTokenToRenew *string `json:"accessTokenToRenew,omitempty"` - // Public client application id. - ClientID string `json:"clientId"` - // Whether the broker may show interaction. - Interaction EntraTokenInteraction `json:"interaction"` - // Broker redirect URI registered for the client. Required: the OneAuth broker validates a - // non-empty, registered redirect URI for the public client (MSAL broker registration), so - // this is not a browser-flow vestige and cannot be omitted. - RedirectURI string `json:"redirectUri"` - // Exact delegated scopes to request. - Scopes []string `json:"scopes"` - // Tenant id or tenant selector, such as common or organizations. - TenantID string `json:"tenantId"` -} - -// Result of a OneAuth token acquisition. -// Experimental: EntraTokenAcquireResult is part of an experimental API and may change or be -// removed. -type EntraTokenAcquireResult interface { - entraTokenAcquireResult() - Status() EntraTokenAcquireResultStatus -} - -type RawEntraTokenAcquireResultData struct { - Discriminator EntraTokenAcquireResultStatus - Raw json.RawMessage -} - -func (RawEntraTokenAcquireResultData) entraTokenAcquireResult() {} -func (r RawEntraTokenAcquireResultData) Status() EntraTokenAcquireResultStatus { - return r.Discriminator -} - -type EntraTokenAcquireResultInteractionRequired struct { -} - -func (EntraTokenAcquireResultInteractionRequired) entraTokenAcquireResult() {} -func (EntraTokenAcquireResultInteractionRequired) Status() EntraTokenAcquireResultStatus { - return EntraTokenAcquireResultStatusInteractionRequired -} - -type EntraTokenAcquireResultOk struct { - // Opaque access token. - AccessToken string `json:"accessToken"` - // Opaque OneAuth account id, when supplied by the broker. - AccountID *string `json:"accountId,omitempty"` - // Expiry as milliseconds since Unix epoch, when supplied by OneAuth. - ExpiresOnTimestamp *float64 `json:"expiresOnTimestamp,omitempty"` -} - -func (EntraTokenAcquireResultOk) entraTokenAcquireResult() {} -func (EntraTokenAcquireResultOk) Status() EntraTokenAcquireResultStatus { - return EntraTokenAcquireResultStatusOk -} - // Hosting capabilities and session capacity advertised by an environment. // Experimental: EnvironmentCapabilities is part of an experimental API and may change or be // removed. @@ -9576,6 +9585,26 @@ type ModelApplyStartupOverlayRequest struct { ServerManagedModel *string `json:"serverManagedModel,omitempty"` } +// Provider-reported model artifact metadata. +// Experimental: ModelArtifactDetails is part of an experimental API and may change or be +// removed. +type ModelArtifactDetails struct { + // Provider-reported model architecture. + Architecture *string `json:"architecture,omitempty"` + // Provider-reported model families. + Families []string `json:"families,omitzero"` + // Primary model family. + Family *string `json:"family,omitempty"` + // Artifact format, such as `gguf`. + Format *string `json:"format,omitempty"` + // Provider-reported parameter count label. + ParameterSize *string `json:"parameterSize,omitempty"` + // Provider-reported quantization label. + Quantization *string `json:"quantization,omitempty"` + // Provider-reported tokenizer. + Tokenizer *string `json:"tokenizer,omitempty"` +} + // Billing information // Experimental: ModelBilling is part of an experimental API and may change or be removed. type ModelBilling struct { @@ -9771,6 +9800,9 @@ type ModelCapabilitiesSupports struct { AdaptiveThinking *AdaptiveThinkingSupport `json:"adaptive_thinking,omitempty"` // Whether this model supports reasoning effort configuration ReasoningEffort *bool `json:"reasoningEffort,omitempty"` + // Whether the model supports provider-native thinking. Independent of configurable + // reasoning effort; omission means unknown. + Thinking *bool `json:"thinking,omitempty"` // Whether this model supports canonical tool calling ToolCalls *bool `json:"toolCalls,omitempty"` // Whether this model supports vision/image input @@ -9834,6 +9866,106 @@ type ModelPolicy struct { Terms *string `json:"terms,omitempty"` } +// Normalized model-provider adapter definitions available to the session, not discovered +// instances. +// Experimental: ModelProviderAdapterCatalog is part of an experimental API and may change +// or be removed. +type ModelProviderAdapterCatalog struct { + // Available provider adapters ordered by adapterId. + Providers []ModelProviderAdapterDescriptor `json:"providers"` +} + +// A normalized model-provider adapter in the session's effective catalog. +// Experimental: ModelProviderAdapterDescriptor is part of an experimental API and may +// change or be removed. +type ModelProviderAdapterDescriptor struct { + // Stable opaque identity for routing to this adapter. Unique in the effective catalog, + // independent of live registration generations. + AdapterID string `json:"adapterId"` + // Adapter-declared policy for passive and automatic discovery. + AutomaticDiscovery ModelProviderAutomaticDiscoveryPolicy `json:"automaticDiscovery"` + // Human-readable provider name. + DisplayName string `json:"displayName"` + // Operations supported by this provider adapter. + Operations []ModelProviderAdapterOperationDescriptor `json:"operations"` + // Attribution for the adapter itself. + Provenance ModelProviderAttribution `json:"provenance"` + // Descriptive provider family, such as `ollama`. Different adapters may have the same + // family; use adapterId for routing. + ProviderKind string `json:"providerKind"` +} + +// An operation supported by a model-provider adapter. +// Experimental: ModelProviderAdapterOperationDescriptor is part of an experimental API and +// may change or be removed. +type ModelProviderAdapterOperationDescriptor struct { + // Optional self-contained JSON Schema Draft 7 for non-null discovery input. Only supported + // on discover. No external references are resolved. Omitted or null input selects defaults + // when requiresInput is false. Without a schema, the adapter validates supplied input. + InputSchema any `json:"inputSchema,omitempty"` + // Supported operation name: `discover`, `getStatus`, or `models.list`. Unknown names and + // duplicate declarations are rejected. + Name string `json:"name"` +} + +// Contributor attribution, independent of routing identity and authorization. +// Experimental: ModelProviderAttribution is part of an experimental API and may change or +// be removed. +type ModelProviderAttribution struct { + // Human-readable contributor name, not the adapter display name. + OwnerDisplayName *string `json:"ownerDisplayName,omitempty"` + // Stable contributor identifier. Required and nonblank for extension and custom sources; + // optional for built-in and configured sources. Does not grant authority. + OwnerID *string `json:"ownerId,omitempty"` + // Kind of component that supplied the adapter. Attribution does not confer authority. + Source ModelProviderProvenanceSource `json:"source"` +} + +// Adapter-declared policy that tells clients whether discovery may run automatically. +// Experimental: ModelProviderAutomaticDiscoveryPolicy is part of an experimental API and +// may change or be removed. +type ModelProviderAutomaticDiscoveryPolicy struct { + // Whether automatic discovery is allowed, limited to configured providers, or explicit-only. + Mode ModelProviderAutomaticDiscoveryMode `json:"mode"` + // Maximum network scope used by this adapter during discovery. + NetworkScope ModelProviderDiscoveryNetworkScope `json:"networkScope"` + // True when discovery requires non-null caller input. Omission or null is rejected before + // adapter execution. When false, omitted or null input selects adapter defaults without + // schema validation. + RequiresInput bool `json:"requiresInput"` + // True when the adapter must be enabled by a trusted owner, such as a trusted extension, + // before automatic discovery may run. + RequiresTrust bool `json:"requiresTrust"` +} + +// Provider configuration prepared from a discovered model. Preparing a plan changes +// nothing: it neither registers the model with the session nor writes durable +// configuration. To apply it, pass `provider` and `model` to `session.provider.add`, +// omitting whichever the dispositions report as already configured. +// Experimental: ModelProviderConfigurationPlan is part of an experimental API and may +// change or be removed. +type ModelProviderConfigurationPlan struct { + // Model definition prepared from the discovered model. Capability fields the provider did + // not report stay omitted rather than being asserted false. + Model ProviderModelConfig `json:"model"` + // Whether `model` still needs to be registered. When `alreadyConfigured`, `selectionId` is + // already registered and the caller can select it without adding anything. + ModelDisposition ModelProviderConfigurationDisposition `json:"modelDisposition"` + // Provider connection prepared from the instance's inference metadata. Carries no + // credential; supply one if the endpoint requires it. + Provider NamedProviderConfig `json:"provider"` + // Whether `provider` still needs to be registered. When `alreadyConfigured`, a provider + // with the same endpoint is already registered and `provider` restates it under its + // existing name; adding it again is rejected as a duplicate. + ProviderDisposition ModelProviderConfigurationDisposition `json:"providerDisposition"` + // Provider-qualified selection id (`provider/id`) to pass to `switchTo` once the plan is + // applied. + SelectionID string `json:"selectionId"` + // Non-fatal warnings carried over from the discovered model, such as capabilities the + // provider did not report. + Warnings []ModelProviderWarning `json:"warnings"` +} + // One model provider available to the session — the model analog of the account // `ProviderDescriptor`. Opaque id/label/kind plus a stable ordering; central code never // branches on kind. @@ -9850,6 +9982,126 @@ type ModelProviderDescriptor struct { Ordering int64 `json:"ordering"` } +// Provider discovery parameters. +// Experimental: ModelProviderDiscoverRequest is part of an experimental API and may change +// or be removed. +type ModelProviderDiscoverRequest struct { + // Opaque adapter identity returned by `session.providers.getCatalog`. + AdapterID string `json:"adapterId"` + // Provider-specific JSON input. Omission or null selects adapter defaults unless + // requiresInput is true. Non-null input is validated against the advertised Draft 7 schema + // when present; otherwise validation belongs to the adapter. + Input any `json:"input,omitempty"` +} + +// Provider instances found by a discovery operation. +// Experimental: ModelProviderDiscoverResult is part of an experimental API and may change +// or be removed. +type ModelProviderDiscoverResult struct { + // Discovered provider instances. Empty when passive default discovery finds no reachable + // provider. + Instances []ModelProviderInstance `json:"instances"` + // Typed operation outcome. Passive discovery can return `absent` with an empty instance + // list. + Outcome ModelProviderOperationOutcome `json:"outcome"` +} + +// Provider status request parameters. +// Experimental: ModelProviderGetStatusRequest is part of an experimental API and may change +// or be removed. +type ModelProviderGetStatusRequest struct { + // Provider instance reference returned by discovery. + Instance ModelProviderInstanceReference `json:"instance"` +} + +// A normalized model-provider instance discovered by the runtime. +// Experimental: ModelProviderInstance is part of an experimental API and may change or be +// removed. +type ModelProviderInstance struct { + // Human-readable instance name. + DisplayName string `json:"displayName"` + // Inference API endpoint when the provider exposes one separately from its management + // endpoint. + InferenceEndpoint *string `json:"inferenceEndpoint,omitempty"` + // Transport to use for inference against this instance. + InferenceTransport *ProviderEndpointTransport `json:"inferenceTransport,omitempty"` + // Provider family to use for inference against this instance. + InferenceType *ProviderEndpointType `json:"inferenceType,omitempty"` + // Wire API to use for inference against this instance, when required by the provider family. + InferenceWireAPI *ProviderEndpointWireAPI `json:"inferenceWireApi,omitempty"` + // Attribution for the adapter that produced this instance. + Provenance ModelProviderProvenance `json:"provenance"` + // Self-contained reference for subsequent provider operations. + Reference ModelProviderInstanceReference `json:"reference"` +} + +// Serializable reference to a discovered provider instance. +// Experimental: ModelProviderInstanceReference is part of an experimental API and may +// change or be removed. +type ModelProviderInstanceReference struct { + // Stable opaque identity of the adapter that owns this reference. Must be present in the + // target session's effective catalog. + AdapterID string `json:"adapterId"` + // Stable instance identifier derived by the provider adapter, such as + // `ollama:{normalizedEndpoint}`. + ID string `json:"id"` + // Absolute provider management URI. The adapter validates normalization, supported schemes, + // and permission to access it against its bound configuration; a reference does not grant + // authority. + ManagementEndpoint string `json:"managementEndpoint"` + // Descriptive provider family. Must match the selected adapter; not a routing key. + ProviderKind string `json:"providerKind"` +} + +// Provider model inventory request parameters. +// Experimental: ModelProviderModelsListRequest is part of an experimental API and may +// change or be removed. +type ModelProviderModelsListRequest struct { + // Provider instance reference returned by discovery. + Instance ModelProviderInstanceReference `json:"instance"` +} + +// Typed provider-operation outcome. Use the code for control flow and the optional message +// for display. +// Experimental: ModelProviderOperationOutcome is part of an experimental API and may change +// or be removed. +type ModelProviderOperationOutcome struct { + // Machine-readable operation outcome. + Code ModelProviderOperationOutcomeCode `json:"code"` + // Human-readable detail for non-success outcomes. + Message *string `json:"message,omitempty"` +} + +// A discovered instance and one of its models to translate into provider configuration. +// Pass back the instance and model as returned by `session.providers.discover` and +// `session.providers.models.list`. +// Experimental: ModelProviderPrepareConfigurationRequest is part of an experimental API and +// may change or be removed. +type ModelProviderPrepareConfigurationRequest struct { + // The discovered instance that serves the model. + Instance ModelProviderInstance `json:"instance"` + // The discovered model to configure. + Model DiscoveredModel `json:"model"` +} + +// Attribution for the adapter that produced a provider row. +// Experimental: ModelProviderProvenance is part of an experimental API and may change or be +// removed. +type ModelProviderProvenance struct { + // Stable opaque adapter identity from the effective catalog. Treat this as a whole + // identifier, not a parseable owner or kind. + AdapterID string `json:"adapterId"` + // Human-readable contributor name, not the adapter display name. + OwnerDisplayName *string `json:"ownerDisplayName,omitempty"` + // Stable contributor identifier when the adapter has an owner outside the runtime. + // Independent of the contribution mechanism and not a routing key. + OwnerID *string `json:"ownerId,omitempty"` + // Descriptive provider family that produced this row; not a routing key. + ProviderKind string `json:"providerKind"` + // Kind of component that supplied the adapter. + Source ModelProviderProvenanceSource `json:"source"` +} + // A neutral reference to the model provider that produced a model: an opaque id, a // human-readable label, and the provider kind. Carried on each enumerated Model so // consumers can group by provider without reaching into a provider-shaped internal type. @@ -9866,6 +10118,30 @@ type ModelProviderRef struct { Label string `json:"label"` } +// Current health information for a provider instance. +// Experimental: ModelProviderStatus is part of an experimental API and may change or be +// removed. +type ModelProviderStatus struct { + // Normalized provider instance. + Instance ModelProviderInstance `json:"instance"` + // Typed operation outcome. + Outcome ModelProviderOperationOutcome `json:"outcome"` + // Open provider status value, such as `healthy`, `unreachable`, or `notInstalled`. + Status string `json:"status"` + // Provider-reported version. + Version *string `json:"version,omitempty"` +} + +// A non-fatal provider observation warning. +// Experimental: ModelProviderWarning is part of an experimental API and may change or be +// removed. +type ModelProviderWarning struct { + // Machine-readable warning code. + Code string `json:"code"` + // Human-readable warning message. + Message string `json:"message"` +} + // Host-supplied exact model selection IDs to allow for this running session. CAPI IDs are // intersected with repository `.github/allowed_models.txt` policy; provider-qualified IDs // remain exempt from repository-only policy but are restricted by this host list. Omit or @@ -13638,6 +13914,41 @@ type SandboxMaskedEnvVar struct { InjectHosts []string `json:"injectHosts"` } +// Result of creating the persistent certificate authority of the sandbox credential proxy. +// Experimental: SandboxProxyCaCreateResult is part of an experimental API and may change or +// be removed. +type SandboxProxyCaCreateResult struct { + // Absolute path of the public certificate of the certificate authority, in PEM format. + CertificatePath string `json:"certificatePath"` +} + +// Identifies the credential hosts that the persistent certificate authority of the sandbox +// credential proxy must cover. The runtime always adds the hosts from the saved user +// settings. +// Experimental: SandboxProxyCaRequest is part of an experimental API and may change or be +// removed. +type SandboxProxyCaRequest struct { + // The sandbox configuration that the host gives its sessions. The runtime reads the + // credential hosts from `auth` and `credentials`; it ignores `enabled` and the other fields. + SandboxConfig *SandboxConfig `json:"sandboxConfig,omitempty"` +} + +// Status of the persistent certificate authority of the sandbox credential proxy. +// Experimental: SandboxProxyCaStatus is part of an experimental API and may change or be +// removed. +type SandboxProxyCaStatus struct { + // Whether this process can add the certificate authority to OS trust without credentials + // from a different user. False where OS trust is unsupported, and on Windows when the + // process cannot elevate itself to write the machine trust store. When false, do not offer + // to set up the certificate authority. + CanInstall bool `json:"canInstall"` + // Human-readable reason for the state. On `installed` or `notInstalled`, present only when + // the certificate authority must be rotated, and then says why. + Detail *string `json:"detail,omitempty"` + // The state of the certificate authority. + State SandboxProxyCaState `json:"state"` +} + // Register an absolute-time scheduled prompt. // Experimental: ScheduleAddAtRequest is part of an experimental API and may change or be // removed. @@ -14353,6 +14664,9 @@ type SessionFSError struct { Code SessionFSErrorCode `json:"code"` // Free-form detail about the error, for logging/diagnostics Message *string `json:"message,omitempty"` + // For failed writeFile requests only: true if the provider changed the target before + // failing. Omit when unknown or unchanged. + WriteChanged *bool `json:"writeChanged,omitempty"` } // Path to test for existence in the client-provided session filesystem. @@ -14441,6 +14755,26 @@ type SessionFSReaddirWithTypesResult struct { Error *SessionFSError `json:"error,omitempty"` } +// Path of the binary file to read from the client-provided session filesystem. +// Experimental: SessionFSReadFileBytesRequest is part of an experimental API and may change +// or be removed. +type SessionFSReadFileBytesRequest struct { + // Path using SessionFs conventions + Path string `json:"path"` + // Target session identifier + SessionID string `json:"sessionId"` +} + +// File bytes as standard base64, or a filesystem error if the read failed. +// Experimental: SessionFSReadFileBytesResult is part of an experimental API and may change +// or be removed. +type SessionFSReadFileBytesResult struct { + // Exact file bytes encoded as standard base64 + Content string `json:"content"` + // Describes a filesystem error. + Error *SessionFSError `json:"error,omitempty"` +} + // Path of the file to read from the client-provided session filesystem. // Experimental: SessionFSReadFileRequest is part of an experimental API and may change or // be removed. @@ -14493,6 +14827,9 @@ type SessionFSRmRequest struct { // Experimental: SessionFSSetProviderCapabilities is part of an experimental API and may // change or be removed. type SessionFSSetProviderCapabilities struct { + // Whether the provider supports binary reads and writes through sessionFs.readFileBytes and + // sessionFs.writeFileBytes + Binary *bool `json:"binary,omitempty"` // Whether the provider supports SQLite query/exists operations Sqlite *bool `json:"sqlite,omitempty"` } @@ -14647,6 +14984,21 @@ type SessionFSStatResult struct { Size int64 `json:"size"` } +// File path, standard-base64-encoded bytes to write, and optional mode for the +// client-provided session filesystem. +// Experimental: SessionFSWriteFileBytesRequest is part of an experimental API and may +// change or be removed. +type SessionFSWriteFileBytesRequest struct { + // Exact file bytes encoded as standard base64 + Content string `json:"content"` + // Optional POSIX-style mode for newly created files + Mode *int64 `json:"mode,omitempty"` + // Path using SessionFs conventions + Path string `json:"path"` + // Target session identifier + SessionID string `json:"sessionId"` +} + // File path, content to write, and optional mode for the client-provided session filesystem. // Experimental: SessionFSWriteFileRequest is part of an experimental API and may change or // be removed. @@ -14787,11 +15139,6 @@ type SessionInstalledPluginSourceURL struct { URL string `json:"url"` } -// Experimental: SessionInstructionsReloadResult is part of an experimental API and may -// change or be removed. -type SessionInstructionsReloadResult struct { -} - // Baseline data provenance for a prediction. // Experimental: SessionLimitPredictionBaselineData is part of an experimental API and may // change or be removed. @@ -22177,6 +22524,44 @@ const ( CopilotAPITokenAuthInfoHostHTTPSGitHubCom CopilotAPITokenAuthInfoHost = "https://github.com" ) +// Result of reloading a customization component. +// Experimental: CustomizationReloadStatus is part of an experimental API and may change or +// be removed. +type CustomizationReloadStatus string + +const ( + // The component could not be refreshed; other components may still reload. + CustomizationReloadStatusFailed CustomizationReloadStatus = "failed" + // The component was refreshed successfully. + CustomizationReloadStatusReloaded CustomizationReloadStatus = "reloaded" + // The component was not configured, loaded, or eligible for refresh. + CustomizationReloadStatusSkipped CustomizationReloadStatus = "skipped" +) + +// Component of session customization discovery. +// Experimental: CustomizationReloadSubsystem is part of an experimental API and may change +// or be removed. +type CustomizationReloadSubsystem string + +const ( + // Discovered custom agents. + CustomizationReloadSubsystemAgents CustomizationReloadSubsystem = "agents" + // Configured session extensions. + CustomizationReloadSubsystemExtensions CustomizationReloadSubsystem = "extensions" + // Configured session and plugin hooks. + CustomizationReloadSubsystemHooks CustomizationReloadSubsystem = "hooks" + // Session instructions and their cached dynamic context. + CustomizationReloadSubsystemInstructions CustomizationReloadSubsystem = "instructions" + // Loaded MCP server configuration. + CustomizationReloadSubsystemMCP CustomizationReloadSubsystem = "mcp" + // Discovered plugin configuration. + CustomizationReloadSubsystemPlugins CustomizationReloadSubsystem = "plugins" + // Repository metadata and working-directory context. + CustomizationReloadSubsystemRepositoryContext CustomizationReloadSubsystem = "repositoryContext" + // Discovered skills. + CustomizationReloadSubsystemSkills CustomizationReloadSubsystem = "skills" +) + // Kind discriminator for DebugCollectLogsDestination. type DebugCollectLogsDestinationKind string @@ -22351,31 +22736,6 @@ const ( DiscoveredMCPServerTypeStdio DiscoveredMCPServerType = "stdio" ) -// Status discriminator for EntraTokenAcquireResult. -// Experimental: EntraTokenAcquireResultStatus is part of an experimental API and may change -// or be removed. -type EntraTokenAcquireResultStatus string - -const ( - EntraTokenAcquireResultStatusInteractionRequired EntraTokenAcquireResultStatus = "interaction-required" - EntraTokenAcquireResultStatusOk EntraTokenAcquireResultStatus = "ok" -) - -// How far OneAuth may go to acquire the requested token. -// Experimental: EntraTokenInteraction is part of an experimental API and may change or be -// removed. -type EntraTokenInteraction string - -const ( - // Always prompt interactively, bypassing any cached or silently-refreshable token. - EntraTokenInteractionForceInteractive EntraTokenInteraction = "force-interactive" - // Allow interactive acquisition, prompting the user only when a cached or silent token is - // unavailable. - EntraTokenInteractionInteractive EntraTokenInteraction = "interactive" - // Acquire the token without any user interaction, failing if interaction would be required. - EntraTokenInteractionSilent EntraTokenInteraction = "silent" -) - // GitHub Mission Control compute kind. // Experimental: EnvironmentKind is part of an experimental API and may change or be removed. type EnvironmentKind string @@ -23822,6 +24182,53 @@ const ( ModelPolicyStateUnconfigured ModelPolicyState = "unconfigured" ) +// When the runtime may run an adapter without an explicit user action. +// Experimental: ModelProviderAutomaticDiscoveryMode is part of an experimental API and may +// change or be removed. +type ModelProviderAutomaticDiscoveryMode string + +const ( + // The adapter declares that automatic discovery is safe when the other policy fields are + // satisfied. + ModelProviderAutomaticDiscoveryModeAutomatic ModelProviderAutomaticDiscoveryMode = "automatic" + // The adapter may refresh instances the user already configured, but must not scan for new + // instances automatically. + ModelProviderAutomaticDiscoveryModeConfiguredOnly ModelProviderAutomaticDiscoveryMode = "configuredOnly" + // The adapter must run only after an explicit user action. + ModelProviderAutomaticDiscoveryModeExplicit ModelProviderAutomaticDiscoveryMode = "explicit" +) + +// Whether a planned configuration entry is new or already present in the session registry. +// Experimental: ModelProviderConfigurationDisposition is part of an experimental API and +// may change or be removed. +type ModelProviderConfigurationDisposition string + +const ( + // An equivalent entry is already registered; the caller should reuse it rather than adding + // a duplicate. + ModelProviderConfigurationDispositionAlreadyConfigured ModelProviderConfigurationDisposition = "alreadyConfigured" + // No matching entry is registered; the caller should add the entry. + ModelProviderConfigurationDispositionCreate ModelProviderConfigurationDisposition = "create" +) + +// Network reach an adapter may use during discovery. +// Experimental: ModelProviderDiscoveryNetworkScope is part of an experimental API and may +// change or be removed. +type ModelProviderDiscoveryNetworkScope string + +const ( + // Discovery contacts only endpoints the user already configured. + ModelProviderDiscoveryNetworkScopeConfiguredEndpointOnly ModelProviderDiscoveryNetworkScope = "configuredEndpointOnly" + // Discovery may contact remote internet services. + ModelProviderDiscoveryNetworkScopeInternet ModelProviderDiscoveryNetworkScope = "internet" + // Discovery may scan or contact the local network. + ModelProviderDiscoveryNetworkScopeLocalNetwork ModelProviderDiscoveryNetworkScope = "localNetwork" + // Discovery is limited to loopback addresses on the local machine. + ModelProviderDiscoveryNetworkScopeLoopbackOnly ModelProviderDiscoveryNetworkScope = "loopbackOnly" + // Discovery does not contact a network service. + ModelProviderDiscoveryNetworkScopeNone ModelProviderDiscoveryNetworkScope = "none" +) + // The neutral kind of a model provider — the model analog of `AccountKind`. A model // provider is the live, entitled source a model came from; central code never branches on // this beyond a single dispatch. @@ -23838,6 +24245,40 @@ const ( ModelProviderKindLoki ModelProviderKind = "loki" ) +// Typed outcome for a provider operation. +// Experimental: ModelProviderOperationOutcomeCode is part of an experimental API and may +// change or be removed. +type ModelProviderOperationOutcomeCode string + +const ( + // The provider or instance is absent during discovery, status, or model listing. Distinct + // from a successful empty inventory. + ModelProviderOperationOutcomeCodeAbsent ModelProviderOperationOutcomeCode = "absent" + // The operation failed for a reason other than absence or reachability. + ModelProviderOperationOutcomeCodeFailed ModelProviderOperationOutcomeCode = "failed" + // The operation completed successfully; an empty inventory is valid. + ModelProviderOperationOutcomeCodeSuccess ModelProviderOperationOutcomeCode = "success" + // The provider is configured or expected but could not be reached. + ModelProviderOperationOutcomeCodeUnreachable ModelProviderOperationOutcomeCode = "unreachable" +) + +// Kind of component that supplied a provider adapter or row. Attribution does not confer +// authority. +// Experimental: ModelProviderProvenanceSource is part of an experimental API and may change +// or be removed. +type ModelProviderProvenanceSource string + +const ( + // Built into the runtime. + ModelProviderProvenanceSourceBuiltIn ModelProviderProvenanceSource = "builtIn" + // Derived from existing user configuration. + ModelProviderProvenanceSourceConfigured ModelProviderProvenanceSource = "configured" + // Supplied by another trusted contributor. + ModelProviderProvenanceSourceCustom ModelProviderProvenanceSource = "custom" + // Supplied by an extension. + ModelProviderProvenanceSourceExtension ModelProviderProvenanceSource = "extension" +) + // Whether the requested preference was already effective or was accepted for later // transactional activation. // Experimental: ModelSwitchAutoTierStatus is part of an experimental API and may change or @@ -24625,6 +25066,23 @@ const ( SandboxConfigSourceUserEnabled SandboxConfigSource = "user_enabled" ) +// State of the persistent certificate authority of the sandbox credential proxy. +// Experimental: SandboxProxyCaState is part of an experimental API and may change or be +// removed. +type SandboxProxyCaState string + +const ( + // The runtime could not read the certificate authority or the OS trust store. + SandboxProxyCaStateError SandboxProxyCaState = "error" + // OS trust includes the stored certificate authority. + SandboxProxyCaStateInstalled SandboxProxyCaState = "installed" + // OS trust does not include the certificate authority, or none is stored. + SandboxProxyCaStateNotInstalled SandboxProxyCaState = "notInstalled" + // This platform has no supported OS trust store. The proxy uses a per-process certificate + // bundle. + SandboxProxyCaStateUnsupported SandboxProxyCaState = "unsupported" +) + // A session-scoped sandbox transition applied while handling a slash command // Experimental: SandboxSessionChange is part of an experimental API and may change or be // removed. @@ -27413,6 +27871,133 @@ func (a *ServerSandboxAPI) GetHostSupport(ctx context.Context) (*SandboxHostSupp return &result, nil } +// Experimental: ServerSandboxProxyCaAPI contains experimental APIs that may change or be +// removed. +type ServerSandboxProxyCaAPI serverAPI + +// Creates the persistent certificate authority of the sandbox credential proxy if none is +// stored, without changing OS trust, and returns the path of its public certificate. Keeps +// an existing certificate authority, even one that must be rotated. Fails where OS trust is +// unsupported. Trust it with sandbox.proxyCa.trust: the CLI trusts only the hosts in the +// saved user settings, so it refuses a certificate authority that also covers hosts from +// sandboxConfig. +// +// RPC method: sandbox.proxyCa.create. +// +// Parameters: Identifies the credential hosts that the persistent certificate authority of +// the sandbox credential proxy must cover. The runtime always adds the hosts from the saved +// user settings. +// +// Returns: Result of creating the persistent certificate authority of the sandbox +// credential proxy. +func (a *ServerSandboxProxyCaAPI) Create(ctx context.Context, params *SandboxProxyCaRequest) (*SandboxProxyCaCreateResult, error) { + raw, err := a.client.Request(ctx, "sandbox.proxyCa.create", params) + if err != nil { + return nil, err + } + var result SandboxProxyCaCreateResult + if err := json.Unmarshal(raw, &result); err != nil { + return nil, err + } + return &result, nil +} + +// GetStatus reports whether the persistent certificate authority of the sandbox credential +// proxy exists, whether OS trust includes it, and whether it must be rotated. Changes +// nothing. +// +// RPC method: sandbox.proxyCa.getStatus. +// +// Parameters: Identifies the credential hosts that the persistent certificate authority of +// the sandbox credential proxy must cover. The runtime always adds the hosts from the saved +// user settings. +// +// Returns: Status of the persistent certificate authority of the sandbox credential proxy. +func (a *ServerSandboxProxyCaAPI) GetStatus(ctx context.Context, params *SandboxProxyCaRequest) (*SandboxProxyCaStatus, error) { + raw, err := a.client.Request(ctx, "sandbox.proxyCa.getStatus", params) + if err != nil { + return nil, err + } + var result SandboxProxyCaStatus + if err := json.Unmarshal(raw, &result); err != nil { + return nil, err + } + return &result, nil +} + +// Removes the persistent certificate authority of the sandbox credential proxy from OS +// trust. Keeps the stored certificate authority. Can show an OS authentication prompt. +// Sandboxed clients that read only OS trust then reject the proxy; clients that read the +// per-process certificate bundle continue to work. +// +// RPC method: sandbox.proxyCa.remove. +// +// Returns: Status of the persistent certificate authority of the sandbox credential proxy. +func (a *ServerSandboxProxyCaAPI) Remove(ctx context.Context) (*SandboxProxyCaStatus, error) { + raw, err := a.client.Request(ctx, "sandbox.proxyCa.remove", nil) + if err != nil { + return nil, err + } + var result SandboxProxyCaStatus + if err := json.Unmarshal(raw, &result); err != nil { + return nil, err + } + return &result, nil +} + +// Rotate replaces the persistent certificate authority of the sandbox credential proxy with +// a new one for the current credential hosts. If OS trust included the old one, removes it +// and trusts the new one, which can show an OS authentication prompt. Running sandboxed +// tools keep the old certificate authority until they restart. +// +// RPC method: sandbox.proxyCa.rotate. +// +// Parameters: Identifies the credential hosts that the persistent certificate authority of +// the sandbox credential proxy must cover. The runtime always adds the hosts from the saved +// user settings. +// +// Returns: Status of the persistent certificate authority of the sandbox credential proxy. +func (a *ServerSandboxProxyCaAPI) Rotate(ctx context.Context, params *SandboxProxyCaRequest) (*SandboxProxyCaStatus, error) { + raw, err := a.client.Request(ctx, "sandbox.proxyCa.rotate", params) + if err != nil { + return nil, err + } + var result SandboxProxyCaStatus + if err := json.Unmarshal(raw, &result); err != nil { + return nil, err + } + return &result, nil +} + +// Trust adds the persistent certificate authority of the sandbox credential proxy to OS +// trust, so sandboxed clients that read only OS trust accept the proxy. Call create first. +// Refuses a certificate authority that is not constrained to the current credential hosts. +// Can show an OS authentication prompt. +// +// RPC method: sandbox.proxyCa.trust. +// +// Parameters: Identifies the credential hosts that the persistent certificate authority of +// the sandbox credential proxy must cover. The runtime always adds the hosts from the saved +// user settings. +// +// Returns: Status of the persistent certificate authority of the sandbox credential proxy. +func (a *ServerSandboxProxyCaAPI) Trust(ctx context.Context, params *SandboxProxyCaRequest) (*SandboxProxyCaStatus, error) { + raw, err := a.client.Request(ctx, "sandbox.proxyCa.trust", params) + if err != nil { + return nil, err + } + var result SandboxProxyCaStatus + if err := json.Unmarshal(raw, &result); err != nil { + return nil, err + } + return &result, nil +} + +// Experimental: ProxyCa returns experimental APIs that may change or be removed. +func (s *ServerSandboxAPI) ProxyCa() *ServerSandboxProxyCaAPI { + return (*ServerSandboxProxyCaAPI)(s) +} + // Experimental: ServerSecretsAPI contains experimental APIs that may change or be removed. type ServerSecretsAPI serverAPI @@ -28507,35 +29092,6 @@ type internalServerAPI struct { client *jsonrpc2.Client } -// Experimental: InternalServerAccountsAPI contains experimental APIs that may change or be -// removed. -type InternalServerAccountsAPI internalServerAPI - -// AcquireEntraToken acquire a Microsoft Entra access token through the runtime's OneAuth -// broker. Account-scoped because it uses the same native broker as the account stack: a -// trusted host application mints a scoped Entra token for its own use, most notably to -// authenticate to a remote MCP server whose authorization server is Entra ID (in place of -// the generic browser-OAuth flow). -// -// RPC method: accounts.acquireEntraToken. -// -// Parameters: OneAuth token request supplied by a trusted host application. -// -// Returns: Result of a OneAuth token acquisition. -// Internal: AcquireEntraToken is part of the SDK's internal handshake/plumbing; external -// callers should not use it. -func (a *InternalServerAccountsAPI) AcquireEntraToken(ctx context.Context, params *EntraTokenAcquireRequest) (EntraTokenAcquireResult, error) { - raw, err := a.client.Request(ctx, "accounts.acquireEntraToken", params) - if err != nil { - return nil, err - } - result, err := unmarshalEntraTokenAcquireResult(raw) - if err != nil { - return nil, err - } - return result, nil -} - // Experimental: InternalServerHostAPI contains experimental APIs that may change or be // removed. type InternalServerHostAPI internalServerAPI @@ -28820,7 +29376,6 @@ type InternalServerRPC struct { // Reuse a single struct instead of allocating one for each service on the heap. common internalServerAPI - Accounts *InternalServerAccountsAPI Host *InternalServerHostAPI Sessions *InternalServerSessionsAPI } @@ -28857,7 +29412,6 @@ func (a *InternalServerRPC) Connect(ctx context.Context, params *ConnectRequest) func NewInternalServerRPC(client *jsonrpc2.Client) *InternalServerRPC { r := &InternalServerRPC{} r.common = internalServerAPI{client: client} - r.Accounts = (*InternalServerAccountsAPI)(&r.common) r.Host = (*InternalServerHostAPI)(&r.common) r.Sessions = (*InternalServerSessionsAPI)(&r.common) return r @@ -29817,21 +30371,24 @@ func (a *ContentExclusionAPI) CheckPaths(ctx context.Context, params *ContentExc // Experimental: CustomizationsAPI contains experimental APIs that may change or be removed. type CustomizationsAPI sessionAPI -// Reloads all repository and user customizations for the active session: instructions, -// plugins and their MCP servers and hooks, custom agents, extensions, and skills. Returns -// diagnostics from the final skill reload. +// Reload for local sessions, reconciles repository context and discovered instructions, +// plugins, skills, agents, hooks, MCP servers, and extensions after files appear or change +// under the working directory. Independent component failures are returned in outcomes and +// errors; a rejected call can have partially applied earlier steps. Remote sessions must +// reload on their agent host instead. The model-facing context is rebuilt on the next turn. // // RPC method: session.customizations.reload. // -// Returns: Diagnostics from reloading skill definitions, with warnings and errors as -// separate lists. -func (a *CustomizationsAPI) Reload(ctx context.Context) (*SkillsLoadDiagnostics, error) { +// Returns: Results of reloading discovered session customizations. Inspect outcomes for +// reloaded, skipped, or failed subsystems; a rejection may follow partial mutation. Changes +// to the model-facing prompt and tools apply on the next turn. +func (a *CustomizationsAPI) Reload(ctx context.Context) (*CustomizationsReloadResult, error) { req := map[string]any{"sessionId": a.sessionID} raw, err := a.client.Request(ctx, "session.customizations.reload", req) if err != nil { return nil, err } - var result SkillsLoadDiagnostics + var result CustomizationsReloadResult if err := json.Unmarshal(raw, &result); err != nil { return nil, err } @@ -30492,17 +31049,20 @@ func (a *InstructionsAPI) GetSources(ctx context.Context) (*InstructionsGetSourc return &result, nil } -// Reload invalidates cached custom-instruction discovery so subsequent turns and source -// reads observe instruction files currently on disk. +// Reload for local sessions, invalidates instruction discovery and the model-facing prompt, +// then returns freshly discovered sources. The updated prompt takes effect on the next +// turn. Remote sessions must reload on their agent host instead. // // RPC method: session.instructions.reload. -func (a *InstructionsAPI) Reload(ctx context.Context) (*SessionInstructionsReloadResult, error) { +// +// Returns: Instruction sources loaded for the session, in merge order. +func (a *InstructionsAPI) Reload(ctx context.Context) (*InstructionsGetSourcesResult, error) { req := map[string]any{"sessionId": a.sessionID} raw, err := a.client.Request(ctx, "session.instructions.reload", req) if err != nil { return nil, err } - var result SessionInstructionsReloadResult + var result InstructionsGetSourcesResult if err := json.Unmarshal(raw, &result); err != nil { return nil, err } @@ -33542,6 +34102,146 @@ func (a *ProviderAPI) Withdraw(ctx context.Context, params *ProviderWithdrawRequ return &result, nil } +// Experimental: ProvidersAPI contains experimental APIs that may change or be removed. +type ProvidersAPI sessionAPI + +// Discovers reachable instances using an adapter from this session's effective provider +// catalog and provider-specific discovery input. +// +// RPC method: session.providers.discover. +// +// Parameters: Provider discovery parameters. +// +// Returns: Provider instances found by a discovery operation. +func (a *ProvidersAPI) Discover(ctx context.Context, params *ModelProviderDiscoverRequest) (*ModelProviderDiscoverResult, error) { + req := map[string]any{"sessionId": a.sessionID} + if params != nil { + req["adapterId"] = params.AdapterID + if params.Input != nil { + req["input"] = params.Input + } + } + raw, err := a.client.Request(ctx, "session.providers.discover", req) + if err != nil { + return nil, err + } + var result ModelProviderDiscoverResult + if err := json.Unmarshal(raw, &result); err != nil { + return nil, err + } + return &result, nil +} + +// GetCatalog returns adapter definitions and supported operations in this session's +// effective provider catalog, without running discovery. Does not list provider instances +// or select inference models. +// +// RPC method: session.providers.getCatalog. +// +// Returns: Normalized model-provider adapter definitions available to the session, not +// discovered instances. +func (a *ProvidersAPI) GetCatalog(ctx context.Context) (*ModelProviderAdapterCatalog, error) { + req := map[string]any{"sessionId": a.sessionID} + raw, err := a.client.Request(ctx, "session.providers.getCatalog", req) + if err != nil { + return nil, err + } + var result ModelProviderAdapterCatalog + if err := json.Unmarshal(raw, &result); err != nil { + return nil, err + } + return &result, nil +} + +// GetStatus gets current health and version information for a discovered model-provider +// instance. +// +// RPC method: session.providers.getStatus. +// +// Parameters: Provider status request parameters. +// +// Returns: Current health information for a provider instance. +func (a *ProvidersAPI) GetStatus(ctx context.Context, params *ModelProviderGetStatusRequest) (*ModelProviderStatus, error) { + req := map[string]any{"sessionId": a.sessionID} + if params != nil { + req["instance"] = params.Instance + } + raw, err := a.client.Request(ctx, "session.providers.getStatus", req) + if err != nil { + return nil, err + } + var result ModelProviderStatus + if err := json.Unmarshal(raw, &result); err != nil { + return nil, err + } + return &result, nil +} + +// Experimental: ProvidersModelsAPI contains experimental APIs that may change or be removed. +type ProvidersModelsAPI sessionAPI + +// Lists models installed or otherwise available from a discovered model-provider instance. +// +// RPC method: session.providers.models.list. +// +// Parameters: Provider model inventory request parameters. +// +// Returns: Models offered for agent conversations by one provider instance. Adapters +// exclude known-incompatible models, but retain candidates with unknown capabilities. +// Listing does not guarantee compatibility. +func (a *ProvidersModelsAPI) List(ctx context.Context, params *ModelProviderModelsListRequest) (*DiscoveredModelList, error) { + req := map[string]any{"sessionId": a.sessionID} + if params != nil { + req["instance"] = params.Instance + } + raw, err := a.client.Request(ctx, "session.providers.models.list", req) + if err != nil { + return nil, err + } + var result DiscoveredModelList + if err := json.Unmarshal(raw, &result); err != nil { + return nil, err + } + return &result, nil +} + +// PrepareConfiguration translates a discovered model into the provider and model +// configuration needed to use it, and reports whether each is already registered in this +// session. Prepares only: it registers nothing, writes nothing, and performs no provider +// requests. +// +// RPC method: session.providers.models.prepareConfiguration. +// +// Parameters: A discovered instance and one of its models to translate into provider +// configuration. Pass back the instance and model as returned by +// `session.providers.discover` and `session.providers.models.list`. +// +// Returns: Provider configuration prepared from a discovered model. Preparing a plan +// changes nothing: it neither registers the model with the session nor writes durable +// configuration. To apply it, pass `provider` and `model` to `session.provider.add`, +// omitting whichever the dispositions report as already configured. +func (a *ProvidersModelsAPI) PrepareConfiguration(ctx context.Context, params *ModelProviderPrepareConfigurationRequest) (*ModelProviderConfigurationPlan, error) { + req := map[string]any{"sessionId": a.sessionID} + if params != nil { + req["instance"] = params.Instance + req["model"] = params.Model + } + raw, err := a.client.Request(ctx, "session.providers.models.prepareConfiguration", req) + if err != nil { + return nil, err + } + var result ModelProviderConfigurationPlan + if err := json.Unmarshal(raw, &result); err != nil { + return nil, err + } + return &result, nil +} + +// Experimental: Models returns experimental APIs that may change or be removed. +func (s *ProvidersAPI) Models() *ProvidersModelsAPI { + return (*ProvidersModelsAPI)(s) +} + // Experimental: QueueAPI contains experimental APIs that may change or be removed. type QueueAPI sessionAPI @@ -36024,6 +36724,7 @@ type SessionRPC struct { Plan *PlanAPI Plugins *PluginsAPI Provider *ProviderAPI + Providers *ProvidersAPI Queue *QueueAPI Remote *RemoteAPI Sandbox *SandboxAPI @@ -36361,6 +37062,7 @@ func NewSessionRPC(client *jsonrpc2.Client, sessionID string) *SessionRPC { r.Plan = (*PlanAPI)(&r.common) r.Plugins = (*PluginsAPI)(&r.common) r.Provider = (*ProviderAPI)(&r.common) + r.Providers = (*ProvidersAPI)(&r.common) r.Queue = (*QueueAPI)(&r.common) r.Remote = (*RemoteAPI)(&r.common) r.Sandbox = (*SandboxAPI)(&r.common) @@ -37335,6 +38037,94 @@ func (a *InternalSettingsAPI) Snapshot(ctx context.Context) (*SessionSettingsSna return &result, nil } +// Experimental: InternalUIAPI contains experimental APIs that may change or be removed. +type InternalUIAPI internalSessionAPI + +// HandleHumanAskUser resolves a pending elicitation request after direct interaction in the +// trusted in-process client. Only an accepted response to the built-in ask_user tool can +// become trusted human evidence. +// +// RPC method: session.ui.handleHumanAskUser. +// +// Parameters: Pending elicitation request ID and the user's response (accept/decline/cancel +// + form values). +// +// Returns: Indicates whether the elicitation response was accepted; false if it was already +// resolved by another client. +// Internal: HandleHumanAskUser is part of the SDK's internal handshake/plumbing; external +// callers should not use it. +func (a *InternalUIAPI) HandleHumanAskUser(ctx context.Context, params *UIHandlePendingElicitationRequest) (*UIElicitationResult, error) { + req := map[string]any{"sessionId": a.sessionID} + if params != nil { + req["requestId"] = params.RequestID + req["result"] = params.Result + } + raw, err := a.client.Request(ctx, "session.ui.handleHumanAskUser", req) + if err != nil { + return nil, err + } + var result UIElicitationResult + if err := json.Unmarshal(raw, &result); err != nil { + return nil, err + } + return &result, nil +} + +// HandleHumanExitPlanMode resolves a pending `exit_plan_mode.requested` event after direct +// interaction in the trusted in-process client. +// +// RPC method: session.ui.handleHumanExitPlanMode. +// +// Parameters: Request ID of a pending `exit_plan_mode.requested` event and the user's +// response. +// +// Returns: Indicates whether the pending UI request was resolved by this call. +// Internal: HandleHumanExitPlanMode is part of the SDK's internal handshake/plumbing; +// external callers should not use it. +func (a *InternalUIAPI) HandleHumanExitPlanMode(ctx context.Context, params *UIHandlePendingExitPlanModeRequest) (*UIHandlePendingResult, error) { + req := map[string]any{"sessionId": a.sessionID} + if params != nil { + req["requestId"] = params.RequestID + req["response"] = params.Response + } + raw, err := a.client.Request(ctx, "session.ui.handleHumanExitPlanMode", req) + if err != nil { + return nil, err + } + var result UIHandlePendingResult + if err := json.Unmarshal(raw, &result); err != nil { + return nil, err + } + return &result, nil +} + +// HandleHumanUserInput resolves a pending `user_input.requested` event after direct +// interaction in the trusted in-process client. +// +// RPC method: session.ui.handleHumanUserInput. +// +// Parameters: Request ID of a pending `user_input.requested` event and the user's response. +// +// Returns: Indicates whether the pending UI request was resolved by this call. +// Internal: HandleHumanUserInput is part of the SDK's internal handshake/plumbing; external +// callers should not use it. +func (a *InternalUIAPI) HandleHumanUserInput(ctx context.Context, params *UIHandlePendingUserInputRequest) (*UIHandlePendingResult, error) { + req := map[string]any{"sessionId": a.sessionID} + if params != nil { + req["requestId"] = params.RequestID + req["response"] = params.Response + } + raw, err := a.client.Request(ctx, "session.ui.handleHumanUserInput", req) + if err != nil { + return nil, err + } + var result UIHandlePendingResult + if err := json.Unmarshal(raw, &result); err != nil { + return nil, err + } + return &result, nil +} + // Experimental: InternalWorkflowAPI contains experimental APIs that may change or be // removed. type InternalWorkflowAPI internalSessionAPI @@ -37443,6 +38233,7 @@ type InternalSessionRPC struct { Queue *InternalQueueAPI Schedule *InternalScheduleAPI Settings *InternalSettingsAPI + UI *InternalUIAPI Workflow *InternalWorkflowAPI } @@ -37490,6 +38281,7 @@ func NewInternalSessionRPC(client *jsonrpc2.Client, sessionID string) *InternalS r.Queue = (*InternalQueueAPI)(&r.common) r.Schedule = (*InternalScheduleAPI)(&r.common) r.Settings = (*InternalSettingsAPI)(&r.common) + r.UI = (*InternalUIAPI)(&r.common) r.Workflow = (*InternalWorkflowAPI)(&r.common) return r } @@ -37603,6 +38395,14 @@ type SessionFSHandler interface { // // Returns: File content as a UTF-8 string, or a filesystem error if the read failed. ReadFile(request *SessionFSReadFileRequest) (*SessionFSReadFileResult, error) + // ReadFileBytes reads binary file content from the client-provided session filesystem. + // + // RPC method: sessionFs.readFileBytes. + // + // Parameters: Path of the binary file to read from the client-provided session filesystem. + // + // Returns: File bytes as standard base64, or a filesystem error if the read failed. + ReadFileBytes(request *SessionFSReadFileBytesRequest) (*SessionFSReadFileBytesResult, error) // Renames or moves a path in the client-provided session filesystem. // // RPC method: sessionFs.rename. @@ -37670,6 +38470,15 @@ type SessionFSHandler interface { // // Returns: Describes a filesystem error. WriteFile(request *SessionFSWriteFileRequest) (*SessionFSError, error) + // WriteFileBytes writes binary file content to the client-provided session filesystem. + // + // RPC method: sessionFs.writeFileBytes. + // + // Parameters: File path, standard-base64-encoded bytes to write, and optional mode for the + // client-provided session filesystem. + // + // Returns: Describes a filesystem error. + WriteFileBytes(request *SessionFSWriteFileBytesRequest) (*SessionFSError, error) } // Experimental: TasksHandler contains experimental APIs that may change or be removed. @@ -37919,6 +38728,25 @@ func RegisterClientSessionAPIHandlers(client *jsonrpc2.Client, getHandlers func( } return raw, nil }) + client.SetRequestHandler("sessionFs.readFileBytes", func(params json.RawMessage) (json.RawMessage, *jsonrpc2.Error) { + var request SessionFSReadFileBytesRequest + if err := json.Unmarshal(params, &request); err != nil { + return nil, &jsonrpc2.Error{Code: -32602, Message: fmt.Sprintf("Invalid params: %v", err)} + } + handlers := getHandlers(request.SessionID) + if handlers == nil || handlers.SessionFS == nil { + return nil, &jsonrpc2.Error{Code: -32603, Message: fmt.Sprintf("No sessionFs handler registered for session: %s", request.SessionID)} + } + result, err := handlers.SessionFS.ReadFileBytes(&request) + if err != nil { + return nil, clientSessionHandlerError(err) + } + raw, err := json.Marshal(result) + if err != nil { + return nil, &jsonrpc2.Error{Code: -32603, Message: fmt.Sprintf("Failed to marshal response: %v", err)} + } + return raw, nil + }) client.SetRequestHandler("sessionFs.rename", func(params json.RawMessage) (json.RawMessage, *jsonrpc2.Error) { var request SessionFSRenameRequest if err := json.Unmarshal(params, &request); err != nil { @@ -38052,6 +38880,25 @@ func RegisterClientSessionAPIHandlers(client *jsonrpc2.Client, getHandlers func( } return raw, nil }) + client.SetRequestHandler("sessionFs.writeFileBytes", func(params json.RawMessage) (json.RawMessage, *jsonrpc2.Error) { + var request SessionFSWriteFileBytesRequest + if err := json.Unmarshal(params, &request); err != nil { + return nil, &jsonrpc2.Error{Code: -32602, Message: fmt.Sprintf("Invalid params: %v", err)} + } + handlers := getHandlers(request.SessionID) + if handlers == nil || handlers.SessionFS == nil { + return nil, &jsonrpc2.Error{Code: -32603, Message: fmt.Sprintf("No sessionFs handler registered for session: %s", request.SessionID)} + } + result, err := handlers.SessionFS.WriteFileBytes(&request) + if err != nil { + return nil, clientSessionHandlerError(err) + } + raw, err := json.Marshal(result) + if err != nil { + return nil, &jsonrpc2.Error{Code: -32603, Message: fmt.Sprintf("Failed to marshal response: %v", err)} + } + return raw, nil + }) client.SetRequestHandler("tasks.cancel", func(params json.RawMessage) (json.RawMessage, *jsonrpc2.Error) { var request ClientTaskCancelRequest if err := json.Unmarshal(params, &request); err != nil { diff --git a/go/rpc/zrpc_encoding.go b/go/rpc/zrpc_encoding.go index 320292785a..739081b803 100644 --- a/go/rpc/zrpc_encoding.go +++ b/go/rpc/zrpc_encoding.go @@ -2573,69 +2573,6 @@ func (r UnsupportedEnqueueCommandResult) MarshalJSON() ([]byte, error) { }) } -func unmarshalEntraTokenAcquireResult(data []byte) (EntraTokenAcquireResult, error) { - if string(data) == "null" { - return nil, nil - } - type rawUnion struct { - Status EntraTokenAcquireResultStatus `json:"status"` - } - var raw rawUnion - if err := json.Unmarshal(data, &raw); err != nil { - return nil, err - } - - switch raw.Status { - case EntraTokenAcquireResultStatusInteractionRequired: - var d EntraTokenAcquireResultInteractionRequired - if err := json.Unmarshal(data, &d); err != nil { - return nil, err - } - return &d, nil - case EntraTokenAcquireResultStatusOk: - var d EntraTokenAcquireResultOk - if err := json.Unmarshal(data, &d); err != nil { - return nil, err - } - return &d, nil - default: - return &RawEntraTokenAcquireResultData{Discriminator: raw.Status, Raw: data}, nil - } -} - -func (r RawEntraTokenAcquireResultData) MarshalJSON() ([]byte, error) { - if r.Raw != nil { - return r.Raw, nil - } - return json.Marshal(struct { - Status EntraTokenAcquireResultStatus `json:"status"` - }{ - Status: r.Discriminator, - }) -} - -func (r EntraTokenAcquireResultInteractionRequired) MarshalJSON() ([]byte, error) { - type alias EntraTokenAcquireResultInteractionRequired - return json.Marshal(struct { - Status EntraTokenAcquireResultStatus `json:"status"` - alias - }{ - Status: r.Status(), - alias: alias(r), - }) -} - -func (r EntraTokenAcquireResultOk) MarshalJSON() ([]byte, error) { - type alias EntraTokenAcquireResultOk - return json.Marshal(struct { - Status EntraTokenAcquireResultStatus `json:"status"` - alias - }{ - Status: r.Status(), - alias: alias(r), - }) -} - func (r EventLogTypes) MarshalJSON() ([]byte, error) { if r.String != nil { return json.Marshal(r.String) @@ -5557,6 +5494,101 @@ func (r *MCPStartServerRequest) UnmarshalJSON(data []byte) error { return nil } +func unmarshalProtocolMarkerSectionOverride(data []byte) (ProtocolMarkerSectionOverride, error) { + if string(data) == "null" { + return nil, nil + } + type rawUnion struct { + Action ProtocolMarkerSectionOverrideAction `json:"action"` + } + var raw rawUnion + if err := json.Unmarshal(data, &raw); err != nil { + return nil, err + } + + switch raw.Action { + case ProtocolMarkerSectionOverrideActionPreserve: + var d ProtocolMarkerSectionOverridePreserve + if err := json.Unmarshal(data, &d); err != nil { + return nil, err + } + return &d, nil + case ProtocolMarkerSectionOverrideActionTransform: + var d ProtocolMarkerSectionOverrideTransform + if err := json.Unmarshal(data, &d); err != nil { + return nil, err + } + return &d, nil + default: + return &RawProtocolMarkerSectionOverrideData{Discriminator: raw.Action, Raw: data}, nil + } +} + +func (r RawProtocolMarkerSectionOverrideData) MarshalJSON() ([]byte, error) { + if r.Raw != nil { + return r.Raw, nil + } + return json.Marshal(struct { + Action ProtocolMarkerSectionOverrideAction `json:"action"` + }{ + Action: r.Discriminator, + }) +} + +func (r ProtocolMarkerSectionOverridePreserve) MarshalJSON() ([]byte, error) { + type alias ProtocolMarkerSectionOverridePreserve + return json.Marshal(struct { + Action ProtocolMarkerSectionOverrideAction `json:"action"` + alias + }{ + Action: r.Action(), + alias: alias(r), + }) +} + +func (r ProtocolMarkerSectionOverrideTransform) MarshalJSON() ([]byte, error) { + type alias ProtocolMarkerSectionOverrideTransform + return json.Marshal(struct { + Action ProtocolMarkerSectionOverrideAction `json:"action"` + alias + }{ + Action: r.Action(), + alias: alias(r), + }) +} + +func (r ProtocolSectionOverride) MarshalJSON() ([]byte, error) { + if r.ProtocolMarkerSectionOverride != nil { + return json.Marshal(r.ProtocolMarkerSectionOverride) + } + if r.ProtocolStaticSectionOverride != nil { + return json.Marshal(r.ProtocolStaticSectionOverride) + } + return []byte("null"), nil +} + +func (r *ProtocolSectionOverride) UnmarshalJSON(data []byte) error { + if string(data) == "null" { + *r = ProtocolSectionOverride{} + return nil + } + { + value, err := unmarshalProtocolMarkerSectionOverride(data) + if err == nil { + *r = ProtocolSectionOverride{ProtocolMarkerSectionOverride: value} + return nil + } + } + { + var value ProtocolStaticSectionOverride + if err := json.Unmarshal(data, &value); err == nil { + *r = ProtocolSectionOverride{ProtocolStaticSectionOverride: &value} + return nil + } + } + return errors.New("data did not match any union variant for ProtocolSectionOverride") +} + func unmarshalPermissionDecision(data []byte) (PermissionDecision, error) { if string(data) == "null" { return nil, nil @@ -6825,101 +6857,6 @@ func (r *PermissionLocationAddToolApprovalParams) UnmarshalJSON(data []byte) err return nil } -func unmarshalProtocolMarkerSectionOverride(data []byte) (ProtocolMarkerSectionOverride, error) { - if string(data) == "null" { - return nil, nil - } - type rawUnion struct { - Action ProtocolMarkerSectionOverrideAction `json:"action"` - } - var raw rawUnion - if err := json.Unmarshal(data, &raw); err != nil { - return nil, err - } - - switch raw.Action { - case ProtocolMarkerSectionOverrideActionPreserve: - var d ProtocolMarkerSectionOverridePreserve - if err := json.Unmarshal(data, &d); err != nil { - return nil, err - } - return &d, nil - case ProtocolMarkerSectionOverrideActionTransform: - var d ProtocolMarkerSectionOverrideTransform - if err := json.Unmarshal(data, &d); err != nil { - return nil, err - } - return &d, nil - default: - return &RawProtocolMarkerSectionOverrideData{Discriminator: raw.Action, Raw: data}, nil - } -} - -func (r RawProtocolMarkerSectionOverrideData) MarshalJSON() ([]byte, error) { - if r.Raw != nil { - return r.Raw, nil - } - return json.Marshal(struct { - Action ProtocolMarkerSectionOverrideAction `json:"action"` - }{ - Action: r.Discriminator, - }) -} - -func (r ProtocolMarkerSectionOverridePreserve) MarshalJSON() ([]byte, error) { - type alias ProtocolMarkerSectionOverridePreserve - return json.Marshal(struct { - Action ProtocolMarkerSectionOverrideAction `json:"action"` - alias - }{ - Action: r.Action(), - alias: alias(r), - }) -} - -func (r ProtocolMarkerSectionOverrideTransform) MarshalJSON() ([]byte, error) { - type alias ProtocolMarkerSectionOverrideTransform - return json.Marshal(struct { - Action ProtocolMarkerSectionOverrideAction `json:"action"` - alias - }{ - Action: r.Action(), - alias: alias(r), - }) -} - -func (r ProtocolSectionOverride) MarshalJSON() ([]byte, error) { - if r.ProtocolMarkerSectionOverride != nil { - return json.Marshal(r.ProtocolMarkerSectionOverride) - } - if r.ProtocolStaticSectionOverride != nil { - return json.Marshal(r.ProtocolStaticSectionOverride) - } - return []byte("null"), nil -} - -func (r *ProtocolSectionOverride) UnmarshalJSON(data []byte) error { - if string(data) == "null" { - *r = ProtocolSectionOverride{} - return nil - } - { - value, err := unmarshalProtocolMarkerSectionOverride(data) - if err == nil { - *r = ProtocolSectionOverride{ProtocolMarkerSectionOverride: value} - return nil - } - } - { - var value ProtocolStaticSectionOverride - if err := json.Unmarshal(data, &value); err == nil { - *r = ProtocolSectionOverride{ProtocolStaticSectionOverride: &value} - return nil - } - } - return errors.New("data did not match any union variant for ProtocolSectionOverride") -} - func unmarshalPushAttachment(data []byte) (PushAttachment, error) { if string(data) == "null" { return nil, nil diff --git a/go/rpc/zsession_encoding.go b/go/rpc/zsession_encoding.go index ae79439771..fa44772dab 100644 --- a/go/rpc/zsession_encoding.go +++ b/go/rpc/zsession_encoding.go @@ -251,6 +251,12 @@ func (e *SessionEvent) UnmarshalJSON(data []byte) error { return err } e.Data = &d + case SessionEventTypeHumanResponseRecorded: + var d HumanResponseRecordedData + if err := json.Unmarshal(raw.Data, &d); err != nil { + return err + } + e.Data = &d case SessionEventTypeMCPAppToolCallComplete: var d MCPAppToolCallCompleteData if err := json.Unmarshal(raw.Data, &d); err != nil { @@ -887,6 +893,12 @@ func (e *SessionEvent) UnmarshalJSON(data []byte) error { return err } e.Data = &d + case SessionEventTypeToolShellOutput: + var d ToolShellOutputData + if err := json.Unmarshal(raw.Data, &d); err != nil { + return err + } + e.Data = &d case SessionEventTypeToolUserRequested: var d ToolUserRequestedData if err := json.Unmarshal(raw.Data, &d); err != nil { @@ -2643,6 +2655,110 @@ func (r *PermissionCompletedData) UnmarshalJSON(data []byte) error { return nil } +func unmarshalHumanResponseRecordedResponse(data []byte) (HumanResponseRecordedResponse, error) { + if string(data) == "null" { + return nil, nil + } + type rawUnion struct { + ResponseKind HumanResponseRecordedResponseResponseKind `json:"responseKind"` + } + var raw rawUnion + if err := json.Unmarshal(data, &raw); err != nil { + return nil, err + } + + switch raw.ResponseKind { + case HumanResponseRecordedResponseResponseKindAskUser: + var d HumanResponseRecordedResponseAskUser + if err := json.Unmarshal(data, &d); err != nil { + return nil, err + } + return &d, nil + case HumanResponseRecordedResponseResponseKindExitPlanMode: + var d HumanResponseRecordedResponseExitPlanMode + if err := json.Unmarshal(data, &d); err != nil { + return nil, err + } + return &d, nil + case HumanResponseRecordedResponseResponseKindUserInput: + var d HumanResponseRecordedResponseUserInput + if err := json.Unmarshal(data, &d); err != nil { + return nil, err + } + return &d, nil + default: + return &RawHumanResponseRecordedResponse{Discriminator: raw.ResponseKind, Raw: data}, nil + } +} + +func (r RawHumanResponseRecordedResponse) MarshalJSON() ([]byte, error) { + if r.Raw != nil { + return r.Raw, nil + } + return json.Marshal(struct { + ResponseKind HumanResponseRecordedResponseResponseKind `json:"responseKind"` + }{ + ResponseKind: r.Discriminator, + }) +} + +func (r HumanResponseRecordedResponseAskUser) MarshalJSON() ([]byte, error) { + type alias HumanResponseRecordedResponseAskUser + return json.Marshal(struct { + ResponseKind HumanResponseRecordedResponseResponseKind `json:"responseKind"` + alias + }{ + ResponseKind: r.ResponseKind(), + alias: alias(r), + }) +} + +func (r HumanResponseRecordedResponseExitPlanMode) MarshalJSON() ([]byte, error) { + type alias HumanResponseRecordedResponseExitPlanMode + return json.Marshal(struct { + ResponseKind HumanResponseRecordedResponseResponseKind `json:"responseKind"` + alias + }{ + ResponseKind: r.ResponseKind(), + alias: alias(r), + }) +} + +func (r HumanResponseRecordedResponseUserInput) MarshalJSON() ([]byte, error) { + type alias HumanResponseRecordedResponseUserInput + return json.Marshal(struct { + ResponseKind HumanResponseRecordedResponseResponseKind `json:"responseKind"` + alias + }{ + ResponseKind: r.ResponseKind(), + alias: alias(r), + }) +} + +func (r *HumanResponseRecordedData) UnmarshalJSON(data []byte) error { + type rawHumanResponseRecordedData struct { + Actor HumanResponseActor `json:"actor"` + RequestID string `json:"requestId"` + Response json.RawMessage `json:"response"` + ToolCallID *string `json:"toolCallId,omitempty"` + } + var raw rawHumanResponseRecordedData + if err := json.Unmarshal(data, &raw); err != nil { + return err + } + r.Actor = raw.Actor + r.RequestID = raw.RequestID + if raw.Response != nil { + value, err := unmarshalHumanResponseRecordedResponse(raw.Response) + if err != nil { + return err + } + r.Response = value + } + r.ToolCallID = raw.ToolCallID + return nil +} + func (r *SessionExtensionsAttachmentsPushedData) UnmarshalJSON(data []byte) error { type rawSessionExtensionsAttachmentsPushedData struct { Attachments []json.RawMessage `json:"attachments"` diff --git a/go/rpc/zsession_events.go b/go/rpc/zsession_events.go index 068ea9ac0d..b301571f68 100644 --- a/go/rpc/zsession_events.go +++ b/go/rpc/zsession_events.go @@ -97,6 +97,7 @@ const ( SessionEventTypeHookEnd SessionEventType = "hook.end" SessionEventTypeHookProgress SessionEventType = "hook.progress" SessionEventTypeHookStart SessionEventType = "hook.start" + SessionEventTypeHumanResponseRecorded SessionEventType = "human_response.recorded" SessionEventTypeMCPAppToolCallComplete SessionEventType = "mcp_app.tool_call_complete" SessionEventTypeMCPHeadersRefreshCompleted SessionEventType = "mcp.headers_refresh_completed" SessionEventTypeMCPHeadersRefreshRequired SessionEventType = "mcp.headers_refresh_required" @@ -241,20 +242,22 @@ const ( SessionEventTypeSkillInvoked SessionEventType = "skill.invoked" // Experimental: SessionEventTypeSkillInvokedRef identifies an experimental event that may // change or be removed. - SessionEventTypeSkillInvokedRef SessionEventType = "skill.invoked_ref" - SessionEventTypeSubagentCompleted SessionEventType = "subagent.completed" - SessionEventTypeSubagentConfigured SessionEventType = "subagent.configured" - SessionEventTypeSubagentDeselected SessionEventType = "subagent.deselected" - SessionEventTypeSubagentFailed SessionEventType = "subagent.failed" - SessionEventTypeSubagentSelected SessionEventType = "subagent.selected" - SessionEventTypeSubagentStarted SessionEventType = "subagent.started" - SessionEventTypeSystemMessage SessionEventType = "system.message" - SessionEventTypeSystemNotification SessionEventType = "system.notification" - SessionEventTypeToolExecutionComplete SessionEventType = "tool.execution_complete" + SessionEventTypeSkillInvokedRef SessionEventType = "skill.invoked_ref" + SessionEventTypeSubagentCompleted SessionEventType = "subagent.completed" + SessionEventTypeSubagentConfigured SessionEventType = "subagent.configured" + SessionEventTypeSubagentDeselected SessionEventType = "subagent.deselected" + SessionEventTypeSubagentFailed SessionEventType = "subagent.failed" + SessionEventTypeSubagentSelected SessionEventType = "subagent.selected" + SessionEventTypeSubagentStarted SessionEventType = "subagent.started" + SessionEventTypeSystemMessage SessionEventType = "system.message" + SessionEventTypeSystemNotification SessionEventType = "system.notification" + SessionEventTypeToolExecutionComplete SessionEventType = "tool.execution_complete" + // Deprecated: SessionEventTypeToolExecutionPartialResult identifies a deprecated event. SessionEventTypeToolExecutionPartialResult SessionEventType = "tool.execution_partial_result" SessionEventTypeToolExecutionProgress SessionEventType = "tool.execution_progress" SessionEventTypeToolExecutionStart SessionEventType = "tool.execution_start" SessionEventTypeToolSearchActivated SessionEventType = "tool_search.activated" + SessionEventTypeToolShellOutput SessionEventType = "tool.shell_output" SessionEventTypeToolUserRequested SessionEventType = "tool.user_requested" // Experimental: SessionEventTypeUIEphemeralQuery identifies an experimental event that may // change or be removed. @@ -759,6 +762,21 @@ type SubagentSelectedData struct { func (*SubagentSelectedData) sessionEventData() {} func (*SubagentSelectedData) Type() SessionEventType { return SessionEventTypeSubagentSelected } +// Deprecated merged replacement snapshot of shell output. Use tool.shell_output for append-only, stream-tagged output instead. +// +// Deprecated: ToolExecutionPartialResultData is deprecated. +type ToolExecutionPartialResultData struct { + // Merged replacement snapshot from the running shell, not an append-only chunk + PartialOutput string `json:"partialOutput"` + // Tool call ID this partial result belongs to + ToolCallID string `json:"toolCallId"` +} + +func (*ToolExecutionPartialResultData) sessionEventData() {} +func (*ToolExecutionPartialResultData) Type() SessionEventType { + return SessionEventTypeToolExecutionPartialResult +} + // Durable record that a canvas instance is open, used to restore open canvases on cold session resume. Intentionally omits the transient url and availability. // Experimental: SessionCanvasRecordedData is part of an experimental API and may change or be removed. type SessionCanvasRecordedData struct { @@ -793,6 +811,23 @@ type SessionCanvasRemovedData struct { func (*SessionCanvasRemovedData) sessionEventData() {} func (*SessionCanvasRemovedData) Type() SessionEventType { return SessionEventTypeSessionCanvasRemoved } +// Durable request-correlated evidence for a typed response to a runtime-owned question or plan review. +type HumanResponseRecordedData struct { + // Controlled actor provenance established at response ingress. + Actor HumanResponseActor `json:"actor"` + // Request ID of the runtime-owned question or plan review. + RequestID string `json:"requestId"` + // Typed request and response payload. + Response HumanResponseRecordedResponse `json:"response"` + // Tool call ID that opened the request, when present. + ToolCallID *string `json:"toolCallId,omitempty"` +} + +func (*HumanResponseRecordedData) sessionEventData() {} +func (*HumanResponseRecordedData) Type() SessionEventType { + return SessionEventTypeHumanResponseRecorded +} + // Durable session usage checkpoint for reconstructing aggregate accounting on resume type SessionUsageCheckpointData struct { // Internal per-model prompt-cache state used to restore expiration tracking on resume @@ -1802,6 +1837,21 @@ func (*AssistantServerToolProgressData) Type() SessionEventType { return SessionEventTypeAssistantServerToolProgress } +// Live, append-only shell output. Not persisted or replayed to late subscribers. Text is decoded and redacted per chunk; chunks need not contain complete lines. +type ToolShellOutputData struct { + // Zero-based publication sequence across all output streams for this tool call. Not a byte offset or an OS write-order guarantee. + Sequence int64 `json:"sequence"` + // Output source. Omission means stdout. Terminal output has no separate stdout/stderr attribution. + Stream *ToolShellOutputStream `json:"stream,omitempty"` + // New output to append, without synthetic shell-result markers or stream-switch separators + Text string `json:"text"` + // Tool call ID that owns this shell output + ToolCallID string `json:"toolCallId"` +} + +func (*ToolShellOutputData) sessionEventData() {} +func (*ToolShellOutputData) Type() SessionEventType { return SessionEventTypeToolShellOutput } + // Live-only Auto preference recommendation from Copilot API after a successful Auto model call. // Experimental: SessionAutoTierRecommendationData is part of an experimental API and may change or be removed. type SessionAutoTierRecommendationData struct { @@ -1950,6 +2000,8 @@ type SessionModelChangeData struct { PreviousVerbosity *Verbosity `json:"previousVerbosity,omitempty"` // Reasoning effort level after the model change, if applicable ReasoningEffort *string `json:"reasoningEffort,omitempty"` + // Model that owns effort embedded in an authored model selection. Omitted for independent reasoning-effort overrides and legacy events. + ReasoningEffortModel *string `json:"reasoningEffortModel,omitempty"` // Reasoning summary mode after the model change, if applicable ReasoningSummary *ReasoningSummary `json:"reasoningSummary,omitempty"` // Origin of the effective model change, when known. @@ -2625,6 +2677,8 @@ type SessionStartData struct { Producer string `json:"producer"` // Reasoning effort level used for model calls, if applicable (e.g. "none", "low", "medium", "high", "xhigh", "max") ReasoningEffort *string `json:"reasoningEffort,omitempty"` + // Model that owns effort embedded in an authored model selection. Omitted for independent reasoning-effort overrides and legacy events. + ReasoningEffortModel *string `json:"reasoningEffortModel,omitempty"` // Reasoning summary mode used for model calls, if applicable (e.g. "none", "concise", "detailed") ReasoningSummary *ReasoningSummary `json:"reasoningSummary,omitempty"` // Whether this session supports remote steering via GitHub @@ -2703,6 +2757,8 @@ type SessionResumeData struct { EventsFileSizeBytes *int64 `json:"eventsFileSizeBytes,omitempty"` // Reasoning effort level used for model calls, if applicable (e.g. "none", "low", "medium", "high", "xhigh", "max") ReasoningEffort *string `json:"reasoningEffort,omitempty"` + // Model that owns effort embedded in an authored model selection. Omitted for independent reasoning-effort overrides and legacy events. + ReasoningEffortModel *string `json:"reasoningEffortModel,omitempty"` // Reasoning summary mode used for model calls, if applicable (e.g. "none", "concise", "detailed") ReasoningSummary *ReasoningSummary `json:"reasoningSummary,omitempty"` // Whether this session supports remote steering via GitHub @@ -2878,19 +2934,6 @@ func (*AssistantStreamingDeltaData) Type() SessionEventType { return SessionEventTypeAssistantStreamingDelta } -// Streaming tool execution output for incremental result display -type ToolExecutionPartialResultData struct { - // Incremental output chunk from the running tool - PartialOutput string `json:"partialOutput"` - // Tool call ID this partial result belongs to - ToolCallID string `json:"toolCallId"` -} - -func (*ToolExecutionPartialResultData) sessionEventData() {} -func (*ToolExecutionPartialResultData) Type() SessionEventType { - return SessionEventTypeToolExecutionPartialResult -} - // Streaming tool-call input delta for incremental tool-call updates type AssistantToolCallDeltaData struct { // Raw provider tool input fragment to append for this tool call. Function/tool-use providers stream serialized JSON argument text (so newlines inside JSON string values may appear as escaped `\n` until the accumulated JSON is parsed); custom tool calls stream raw custom input. @@ -3083,6 +3126,9 @@ func (*SessionModelDeselectedData) Type() SessionEventType { type ToolExecutionCompleteData struct { // Error details when the tool execution failed Error *ToolExecutionCompleteError `json:"error,omitempty"` + // Experimental. File mutations actually committed by a built-in file editing tool, in execution order. Present on successful edits and on partial failures when earlier mutations were committed. Paths are absolute in the session filesystem namespace. + // Experimental: FileEdits is part of an experimental API and may change or be removed. + FileEdits []ToolExecutionCompleteFileEdit `json:"fileEdits,omitzero"` // Experimental HydraFusion attribution for this tool completion. // Experimental: Fusion is part of an experimental API and may change or be removed. Fusion *FusionAttribution `json:"fusion,omitempty"` @@ -3348,6 +3394,8 @@ func (*SessionWorkspaceFileChangedData) Type() SessionEventType { type AssistantMessageReasoningBlocks struct { // Provider-native reasoning items or content blocks preserved verbatim, in order. A single response can carry several, and provider signatures or identifiers may depend on their exact content and ordering. Blocks []any `json:"blocks,omitzero"` + // Anthropic Messages assistant block ordering preserved when the legacy reasoning-only representation cannot reproduce it exactly. Thinking and text blocks remain verbatim; tool-use entries retain identity and a payload fingerprint when later signed reasoning depends on them, and are hydrated from the message's tool requests during replay. + OrderedBlocks []any `json:"orderedBlocks,omitzero"` // Model provider that produced these reasoning blocks. Provider string `json:"provider"` } @@ -3888,6 +3936,78 @@ type HookEndError struct { Stack *string `json:"stack,omitempty"` } +// Exact runtime-owned question or reviewed plan paired with the typed response that settled it. +type HumanResponseRecordedResponse interface { + humanResponseRecordedResponse() + ResponseKind() HumanResponseRecordedResponseResponseKind +} + +type RawHumanResponseRecordedResponse struct { + Discriminator HumanResponseRecordedResponseResponseKind + Raw json.RawMessage +} + +func (RawHumanResponseRecordedResponse) humanResponseRecordedResponse() {} +func (r RawHumanResponseRecordedResponse) ResponseKind() HumanResponseRecordedResponseResponseKind { + return r.Discriminator +} + +type HumanResponseRecordedResponseAskUser struct { + // Exact answer content accepted from the user. + Content map[string]any `json:"content"` + // Exact question displayed to the user. + Message string `json:"message"` + // Exact response schema displayed to the user. + RequestedSchema ElicitationRequestedSchema `json:"requestedSchema"` +} + +func (HumanResponseRecordedResponseAskUser) humanResponseRecordedResponse() {} +func (HumanResponseRecordedResponseAskUser) ResponseKind() HumanResponseRecordedResponseResponseKind { + return HumanResponseRecordedResponseResponseKindAskUser +} + +type HumanResponseRecordedResponseExitPlanMode struct { + // Actions offered by the plan review UI. + Actions []ExitPlanModeAction `json:"actions"` + // Whether the user approved the reviewed plan. + Approved bool `json:"approved"` + // Whether the selected response requested edit auto-approval. + AutoApproveEdits *bool `json:"autoApproveEdits,omitempty"` + // Exact feedback submitted with the plan decision, when present. + Feedback *string `json:"feedback,omitempty"` + // Exact full plan content available from the review UI. + PlanContent string `json:"planContent"` + // Action the plan review UI recommended. + RecommendedAction ExitPlanModeAction `json:"recommendedAction"` + // Action selected by the user, when applicable. + SelectedAction *ExitPlanModeAction `json:"selectedAction,omitempty"` + // Exact plan summary displayed to the user. + Summary string `json:"summary"` +} + +func (HumanResponseRecordedResponseExitPlanMode) humanResponseRecordedResponse() {} +func (HumanResponseRecordedResponseExitPlanMode) ResponseKind() HumanResponseRecordedResponseResponseKind { + return HumanResponseRecordedResponseResponseKindExitPlanMode +} + +type HumanResponseRecordedResponseUserInput struct { + // Whether the displayed request allowed a free-form answer. + AllowFreeform *bool `json:"allowFreeform,omitempty"` + // Exact selected or free-form answer submitted by the user. + Answer string `json:"answer"` + // Exact choices displayed to the user, when the request offered choices. + Choices []string `json:"choices,omitzero"` + // Exact question displayed to the user. + Question string `json:"question"` + // Whether the answer was typed as free-form text rather than selected from the displayed choices. + WasFreeform bool `json:"wasFreeform"` +} + +func (HumanResponseRecordedResponseUserInput) humanResponseRecordedResponse() {} +func (HumanResponseRecordedResponseUserInput) ResponseKind() HumanResponseRecordedResponseResponseKind { + return HumanResponseRecordedResponseResponseKindUserInput +} + // Set when the underlying tools/call threw an error before returning a CallToolResult type MCPAppToolCallCompleteError struct { // Human-readable error message @@ -5415,6 +5535,15 @@ type ToolExecutionCompleteError struct { Remediation *RemediationAction `json:"remediation,omitempty"` } +// A file mutation that was actually committed by a built-in file editing tool. +// Experimental: ToolExecutionCompleteFileEdit is part of an experimental API and may change or be removed. +type ToolExecutionCompleteFileEdit struct { + // Kind of mutation committed at this path. + Kind ToolExecutionCompleteFileEditKind `json:"kind"` + // Absolute path in the session filesystem namespace. + Path string `json:"path"` +} + // Tool execution result on success type ToolExecutionCompleteResult struct { // Model-facing binary results (base64 inline or size-omitted markers) sent to the LLM for this tool call @@ -6053,6 +6182,27 @@ const ( HandoffSourceTypeRemote HandoffSourceType = "remote" ) +// Controlled provenance for a typed runtime response. Only `human_response`, minted by a trusted direct-interaction ingress, is human authorization evidence. +type HumanResponseActor string + +const ( + // A host or SDK automation submitted the response without direct human interaction. + HumanResponseActorHostAutomation HumanResponseActor = "host_automation" + // A built-in trusted client submitted the response after direct human interaction. + HumanResponseActorHumanResponse HumanResponseActor = "human_response" + // The response came through a legacy or otherwise unattributed ingress. + HumanResponseActorUnknown HumanResponseActor = "unknown" +) + +// ResponseKind discriminator for HumanResponseRecordedResponse. +type HumanResponseRecordedResponseResponseKind string + +const ( + HumanResponseRecordedResponseResponseKindAskUser HumanResponseRecordedResponseResponseKind = "ask_user" + HumanResponseRecordedResponseResponseKindExitPlanMode HumanResponseRecordedResponseResponseKind = "exit_plan_mode" + HumanResponseRecordedResponseResponseKindUserInput HumanResponseRecordedResponseResponseKind = "user_input" +) + // The category of runtime action that enterprise managed settings governed (blocked or capped) type ManagedSettingsEnforcedAction string @@ -6657,6 +6807,19 @@ const ( ToolExecutionCompleteContentTypeText ToolExecutionCompleteContentType = "text" ) +// Kind of file mutation committed by a built-in editing tool. +// Experimental: ToolExecutionCompleteFileEditKind is part of an experimental API and may change or be removed. +type ToolExecutionCompleteFileEditKind string + +const ( + // A file was created. + ToolExecutionCompleteFileEditKindCreate ToolExecutionCompleteFileEditKind = "create" + // A file was deleted. + ToolExecutionCompleteFileEditKindDelete ToolExecutionCompleteFileEditKind = "delete" + // A file was written by an edit operation. + ToolExecutionCompleteFileEditKindEdit ToolExecutionCompleteFileEditKind = "edit" +) + // Allowed values for the `ToolExecutionCompleteToolDescriptionMetaUIVisibility` enumeration. type ToolExecutionCompleteToolDescriptionMetaUIVisibility string @@ -6677,6 +6840,18 @@ const ( ToolExecutionStartToolDescriptionMetaUIVisibilityModel ToolExecutionStartToolDescriptionMetaUIVisibility = "model" ) +// Shell output source. Terminal output has no separate stdout/stderr attribution. +type ToolShellOutputStream string + +const ( + // Output from the shell command's standard error stream. + ToolShellOutputStreamStderr ToolShellOutputStream = "stderr" + // Output from the shell command's standard output stream. This is the default when stream is omitted. + ToolShellOutputStreamStdout ToolShellOutputStream = "stdout" + // Inherently merged output that cannot be attributed separately to stdout or stderr. + ToolShellOutputStreamTerminal ToolShellOutputStream = "terminal" +) + // Lifecycle phase for a Rust-owned ephemeral query stream. // Experimental: UIEphemeralQueryPhase is part of an experimental API and may change or be removed. type UIEphemeralQueryPhase string diff --git a/go/session.go b/go/session.go index 3d610b723c..45113a59ac 100644 --- a/go/session.go +++ b/go/session.go @@ -896,6 +896,26 @@ func (s *Session) handleHooksInvoke(hookType string, rawInput json.RawMessage) ( } return hooks.OnAgentStop(input, invocation) + case "subagentStart": + if hooks.OnSubagentStart == nil { + return nil, nil + } + var input SubagentStartHookInput + if err := json.Unmarshal(rawInput, &input); err != nil { + return nil, fmt.Errorf("invalid hook input: %w", err) + } + return hooks.OnSubagentStart(input, invocation) + + case "subagentStop": + if hooks.OnSubagentStop == nil { + return nil, nil + } + var input SubagentStopHookInput + if err := json.Unmarshal(rawInput, &input); err != nil { + return nil, fmt.Errorf("invalid hook input: %w", err) + } + return hooks.OnSubagentStop(input, invocation) + default: return nil, nil } diff --git a/go/session_fs_provider.go b/go/session_fs_provider.go index 0f653f1a0f..1f00ecc83f 100644 --- a/go/session_fs_provider.go +++ b/go/session_fs_provider.go @@ -5,6 +5,7 @@ package copilot import ( + "encoding/base64" "errors" "os" "time" @@ -12,6 +13,9 @@ import ( "github.com/github/copilot-sdk/go/rpc" ) +const maxSessionFSBinaryBytes = (64*1024*1024 - 1024) / 4 * 3 +const maxSessionFSBinaryContentLength = (maxSessionFSBinaryBytes + 2) / 3 * 4 + // SessionFSProvider is the interface that SDK users implement to provide // a session filesystem. Methods use idiomatic Go error handling: return an // error for failures (the adapter maps os.ErrNotExist → ENOENT automatically). @@ -23,6 +27,7 @@ type SessionFSProvider interface { ReadFile(path string) (string, error) // WriteFile writes content to a file, creating it and parent directories if needed. // mode is an optional POSIX-style permission mode. Pass nil to use the OS default. + // Return a *SessionFSWriteFailure if the failed write changed the target. WriteFile(path string, content string, mode *int) error // AppendFile appends content to a file, creating it and parent directories if needed. // mode is an optional POSIX-style permission mode. Pass nil to use the OS default. @@ -48,6 +53,32 @@ type SessionFSProvider interface { Rename(src string, dest string) error } +// SessionFSWriteFailure reports that a failed WriteFile changed its target. +// Wrap the original error so its cause remains available to errors.Is and errors.As. +type SessionFSWriteFailure struct { + Err error +} + +func (e *SessionFSWriteFailure) Error() string { + if e == nil || e.Err == nil { + return "session filesystem write failed after changing the target" + } + return e.Err.Error() +} +func (e *SessionFSWriteFailure) Unwrap() error { + if e == nil { + return nil + } + return e.Err +} + +// SessionFSBinaryProvider is an optional interface for exact file bytes. +// Declare capabilities.binary only when the provider implements both methods. +type SessionFSBinaryProvider interface { + ReadFileBytes(path string) ([]byte, error) + WriteFileBytes(path string, content []byte, mode *int) error +} + // SessionFSSqliteProvider is an optional interface that a [SessionFSProvider] // may also implement to support per-session SQLite databases. The adapter // checks for this interface at runtime using a type assertion. If the @@ -129,6 +160,23 @@ func (a *sessionFSAdapter) ReadFile(request *rpc.SessionFSReadFileRequest) (*rpc return &rpc.SessionFSReadFileResult{Content: content}, nil } +func (a *sessionFSAdapter) ReadFileBytes(request *rpc.SessionFSReadFileBytesRequest) (*rpc.SessionFSReadFileBytesResult, error) { + provider, ok := a.provider.(SessionFSBinaryProvider) + if !ok { + return &rpc.SessionFSReadFileBytesResult{Error: toSessionFSError(errors.New("binary reads are not supported"))}, nil + } + bytes, err := provider.ReadFileBytes(request.Path) + if err != nil { + return &rpc.SessionFSReadFileBytesResult{Error: toSessionFSError(err)}, nil + } + if len(bytes) > maxSessionFSBinaryBytes { + return &rpc.SessionFSReadFileBytesResult{ + Error: toSessionFSError(errors.New("sessionFs.readFileBytes content exceeds the binary read limit")), + }, nil + } + return &rpc.SessionFSReadFileBytesResult{Content: base64.StdEncoding.EncodeToString(bytes)}, nil +} + func (a *sessionFSAdapter) WriteFile(request *rpc.SessionFSWriteFileRequest) (*rpc.SessionFSError, error) { var mode *int if request.Mode != nil { @@ -136,6 +184,41 @@ func (a *sessionFSAdapter) WriteFile(request *rpc.SessionFSWriteFileRequest) (*r mode = &m } if err := a.provider.WriteFile(request.Path, request.Content, mode); err != nil { + wire := toSessionFSError(err) + var changed *SessionFSWriteFailure + if errors.As(err, &changed) && changed != nil { + value := true + wire.WriteChanged = &value + } + return wire, nil + } + return nil, nil +} + +func (a *sessionFSAdapter) WriteFileBytes(request *rpc.SessionFSWriteFileBytesRequest) (*rpc.SessionFSError, error) { + provider, ok := a.provider.(SessionFSBinaryProvider) + if !ok { + return toSessionFSError(errors.New("binary writes are not supported")), nil + } + if len(request.Content) > maxSessionFSBinaryContentLength { + return toSessionFSError(errors.New("sessionFs.writeFileBytes content exceeds the binary write limit")), nil + } + content, err := base64.StdEncoding.Strict().DecodeString(request.Content) + if err != nil { + return toSessionFSError(err), nil + } + if base64.StdEncoding.EncodeToString(content) != request.Content { + return toSessionFSError(errors.New("invalid sessionFs.writeFileBytes base64 content")), nil + } + if len(content) > maxSessionFSBinaryBytes { + return toSessionFSError(errors.New("sessionFs.writeFileBytes content exceeds the binary write limit")), nil + } + var mode *int + if request.Mode != nil { + m := int(*request.Mode) + mode = &m + } + if err := provider.WriteFileBytes(request.Path, content, mode); err != nil { return toSessionFSError(err), nil } return nil, nil diff --git a/go/session_fs_provider_test.go b/go/session_fs_provider_test.go new file mode 100644 index 0000000000..e0b3d3aba0 --- /dev/null +++ b/go/session_fs_provider_test.go @@ -0,0 +1,64 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. + +package copilot + +import ( + "errors" + "fmt" + "os" + "testing" + + "github.com/github/copilot-sdk/go/rpc" +) + +type failingWriteProvider struct { + SessionFSProvider + err error +} + +func (p failingWriteProvider) WriteFile(string, string, *int) error { return p.err } +func (p failingWriteProvider) AppendFile(string, string, *int) error { return p.err } + +func TestSessionFSWriteFailure(t *testing.T) { + for _, tc := range []struct { + name string + err error + wantChanged bool + wantCode rpc.SessionFSErrorCode + wantMessage string + }{ + {"rejected", errors.New("rejected"), false, rpc.SessionFSErrorCodeUNKNOWN, "rejected"}, + {"partial", &SessionFSWriteFailure{Err: errors.New("disk full")}, true, rpc.SessionFSErrorCodeUNKNOWN, "disk full"}, + {"wrapped", fmt.Errorf("write failed: %w", &SessionFSWriteFailure{Err: os.ErrNotExist}), true, rpc.SessionFSErrorCodeENOENT, "write failed: file does not exist"}, + {"nil cause", &SessionFSWriteFailure{}, true, rpc.SessionFSErrorCodeUNKNOWN, "session filesystem write failed after changing the target"}, + {"nil marker", (*SessionFSWriteFailure)(nil), false, rpc.SessionFSErrorCodeUNKNOWN, "session filesystem write failed after changing the target"}, + } { + t.Run(tc.name, func(t *testing.T) { + adapter := newSessionFSAdapter(failingWriteProvider{err: tc.err}) + wire, err := adapter.WriteFile(&rpc.SessionFSWriteFileRequest{Path: "/file", Content: "data"}) + if err != nil || wire == nil { + t.Fatalf("WriteFile = %v, %v; want wire error", wire, err) + } + if wire.Code != tc.wantCode { + t.Errorf("code = %v; want %v", wire.Code, tc.wantCode) + } + if wire.Message == nil || *wire.Message != tc.wantMessage { + t.Errorf("message = %v; want %q", wire.Message, tc.wantMessage) + } + gotChanged := wire.WriteChanged != nil && *wire.WriteChanged + if gotChanged != tc.wantChanged || !tc.wantChanged && wire.WriteChanged != nil { + t.Errorf("writeChanged = %v; want %v", wire.WriteChanged, tc.wantChanged) + } + }) + } +} + +func TestSessionFSWriteFailureDoesNotMarkAppend(t *testing.T) { + adapter := newSessionFSAdapter(failingWriteProvider{ + err: &SessionFSWriteFailure{Err: errors.New("append failed")}, + }) + wire, err := adapter.AppendFile(&rpc.SessionFSAppendFileRequest{Path: "/file", Content: "data"}) + if err != nil || wire == nil || wire.WriteChanged != nil { + t.Fatalf("AppendFile = %v, %v; want unmarked wire error", wire, err) + } +} diff --git a/go/subagent_hooks_test.go b/go/subagent_hooks_test.go new file mode 100644 index 0000000000..8f68cc49d6 --- /dev/null +++ b/go/subagent_hooks_test.go @@ -0,0 +1,131 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package copilot + +import ( + "encoding/json" + "reflect" + "testing" + "time" +) + +func TestSession_SubagentStartHook(t *testing.T) { + session, cleanup := newTestSession() + defer cleanup() + session.SessionID = "parent-session" + + var captured SubagentStartHookInput + session.registerHooks(&SessionHooks{ + OnSubagentStart: func(input SubagentStartHookInput, invocation HookInvocation) (*SubagentStartHookOutput, error) { + captured = input + if invocation.SessionID != "parent-session" { + t.Errorf("invocation session ID = %q, want parent-session", invocation.SessionID) + } + return &SubagentStartHookOutput{AdditionalContext: "read only the requested file"}, nil + }, + }) + + raw := json.RawMessage(`{"sessionId":"parent-session","timestamp":1700000000123,"cwd":"C:\\work","transcriptPath":"C:\\transcript.jsonl","agentName":"explore","agentDisplayName":"Explore Agent","agentDescription":"Find files"}`) + output, err := session.handleHooksInvoke("subagentStart", raw) + if err != nil { + t.Fatalf("subagentStart dispatch failed: %v", err) + } + if captured.SessionID != "parent-session" || !captured.Timestamp.Equal(time.UnixMilli(1700000000123)) || + captured.WorkingDirectory != `C:\work` || captured.TranscriptPath != `C:\transcript.jsonl` || + captured.AgentName != "explore" || captured.AgentDisplayName != "Explore Agent" || + captured.AgentDescription != "Find files" { + t.Errorf("subagentStart input = %+v", captured) + } + assertHookJSON(t, captured, raw) + assertHookJSON(t, output, json.RawMessage(`{"additionalContext":"read only the requested file"}`)) + + session.registerHooks(&SessionHooks{}) + output, err = session.handleHooksInvoke("subagentStart", raw) + if err != nil || output != nil { + t.Errorf("unregistered subagentStart = (%v, %v), want (nil, nil)", output, err) + } +} + +func TestSession_SubagentStopHook(t *testing.T) { + session, cleanup := newTestSession() + defer cleanup() + session.SessionID = "parent-session" + + raw := json.RawMessage(`{"sessionId":"parent-session","timestamp":1700000000456,"cwd":"C:\\work","transcriptPath":"C:\\transcript.jsonl","agentId":"read-file","agentType":"explore","agentName":"explore","agentDisplayName":"Explore Agent","agentDescription":"Find files","stopReason":"end_turn","response":"Hello from subagent test!"}`) + tests := []struct { + name string + output *SubagentStopHookOutput + want json.RawMessage + }{ + { + name: "block with a reason", + output: &SubagentStopHookOutput{Decision: "block", Reason: "Read the file again"}, + want: json.RawMessage(`{"decision":"block","reason":"Read the file again"}`), + }, + { + name: "replace the response", + output: &SubagentStopHookOutput{ModifiedResponse: String("Changed result")}, + want: json.RawMessage(`{"modifiedResponse":"Changed result"}`), + }, + { + name: "replace with an empty response", + output: &SubagentStopHookOutput{ModifiedResponse: String("")}, + want: json.RawMessage(`{"modifiedResponse":""}`), + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var captured SubagentStopHookInput + session.registerHooks(&SessionHooks{ + OnSubagentStop: func(input SubagentStopHookInput, invocation HookInvocation) (*SubagentStopHookOutput, error) { + captured = input + if invocation.SessionID != "parent-session" { + t.Errorf("invocation session ID = %q, want parent-session", invocation.SessionID) + } + return tt.output, nil + }, + }) + + output, err := session.handleHooksInvoke("subagentStop", raw) + if err != nil { + t.Fatalf("subagentStop dispatch failed: %v", err) + } + if captured.SessionID != "parent-session" || !captured.Timestamp.Equal(time.UnixMilli(1700000000456)) || + captured.WorkingDirectory != `C:\work` || captured.TranscriptPath != `C:\transcript.jsonl` || + captured.AgentID != "read-file" || captured.AgentType != "explore" || + captured.AgentName != "explore" || captured.AgentDisplayName != "Explore Agent" || + captured.AgentDescription != "Find files" || captured.StopReason != "end_turn" || + captured.Response != "Hello from subagent test!" { + t.Errorf("subagentStop input = %+v", captured) + } + assertHookJSON(t, captured, raw) + assertHookJSON(t, output, tt.want) + }) + } + + session.registerHooks(&SessionHooks{}) + output, err := session.handleHooksInvoke("subagentStop", raw) + if err != nil || output != nil { + t.Errorf("unregistered subagentStop = (%v, %v), want (nil, nil)", output, err) + } +} + +func assertHookJSON(t *testing.T, value any, expected json.RawMessage) { + t.Helper() + gotJSON, err := json.Marshal(value) + if err != nil { + t.Fatalf("marshal %T: %v", value, err) + } + var got, want any + if err := json.Unmarshal(gotJSON, &got); err != nil { + t.Fatalf("decode marshaled %T: %v", value, err) + } + if err := json.Unmarshal(expected, &want); err != nil { + t.Fatalf("decode expected JSON: %v", err) + } + if !reflect.DeepEqual(got, want) { + t.Errorf("JSON for %T = %s, want %s", value, gotJSON, expected) + } +} diff --git a/go/types.go b/go/types.go index 41c8f5941c..af4fd783fb 100644 --- a/go/types.go +++ b/go/types.go @@ -373,12 +373,14 @@ const ( SectionToolInstructions = "tool_instructions" // SectionCustomInstructions covers repository and organization custom instructions. SectionCustomInstructions = "custom_instructions" - // SectionRuntimeInstructions targets runtime-provided context and instructions - // (e.g. system notifications, memories, workspace context, mode-specific instructions, - // content-exclusion policy). + // SectionRuntimeInstructions targets runtime-provided system-prompt context and + // instructions, such as system notifications, memories, workspace context, and + // content-exclusion policy. Mode-specific instructions can travel in transition + // messages instead. SectionRuntimeInstructions = "runtime_instructions" // SectionLastInstructions covers end-of-prompt instructions: parallel tool calling, - // persistence, and task completion. + // persistence, task completion, and configured subagent-model guidance when the + // task tool is available. SectionLastInstructions = "last_instructions" ) @@ -1027,6 +1029,98 @@ type AgentStopHookOutput struct { // AgentStopHandler handles agent-stop hook invocations. type AgentStopHandler func(input AgentStopHookInput, invocation HookInvocation) (*AgentStopHookOutput, error) +// SubagentStartHookInput is the input before a subagent's first turn. +type SubagentStartHookInput struct { + SessionID string `json:"sessionId"` + Timestamp time.Time `json:"-"` + WorkingDirectory string `json:"cwd"` + TranscriptPath string `json:"transcriptPath"` + AgentName string `json:"agentName"` + AgentDisplayName string `json:"agentDisplayName,omitempty"` + AgentDescription string `json:"agentDescription,omitempty"` +} + +// MarshalJSON implements json.Marshaler, emitting Timestamp as Unix milliseconds. +func (h SubagentStartHookInput) MarshalJSON() ([]byte, error) { + type alias SubagentStartHookInput + return json.Marshal(&struct { + Timestamp int64 `json:"timestamp"` + alias + }{Timestamp: h.Timestamp.UnixMilli(), alias: alias(h)}) +} + +// UnmarshalJSON implements json.Unmarshaler, parsing Timestamp from Unix milliseconds. +func (h *SubagentStartHookInput) UnmarshalJSON(data []byte) error { + type alias SubagentStartHookInput + aux := &struct { + Timestamp int64 `json:"timestamp"` + *alias + }{alias: (*alias)(h)} + if err := json.Unmarshal(data, aux); err != nil { + return err + } + h.Timestamp = time.UnixMilli(aux.Timestamp) + return nil +} + +// SubagentStartHookOutput provides context prepended to the subagent's initial prompt. +type SubagentStartHookOutput struct { + AdditionalContext string `json:"additionalContext,omitempty"` +} + +// SubagentStartHandler handles subagent-start hook invocations. +type SubagentStartHandler func(input SubagentStartHookInput, invocation HookInvocation) (*SubagentStartHookOutput, error) + +// SubagentStopHookInput is the input after a subagent completes a turn. +type SubagentStopHookInput struct { + SessionID string `json:"sessionId"` + Timestamp time.Time `json:"-"` + WorkingDirectory string `json:"cwd"` + TranscriptPath string `json:"transcriptPath"` + AgentID string `json:"agentId,omitempty"` + AgentType string `json:"agentType"` + AgentName string `json:"agentName"` + AgentDisplayName string `json:"agentDisplayName,omitempty"` + AgentDescription string `json:"agentDescription,omitempty"` + StopReason string `json:"stopReason"` + Response string `json:"response"` +} + +// MarshalJSON implements json.Marshaler, emitting Timestamp as Unix milliseconds. +func (h SubagentStopHookInput) MarshalJSON() ([]byte, error) { + type alias SubagentStopHookInput + return json.Marshal(&struct { + Timestamp int64 `json:"timestamp"` + alias + }{Timestamp: h.Timestamp.UnixMilli(), alias: alias(h)}) +} + +// UnmarshalJSON implements json.Unmarshaler, parsing Timestamp from Unix milliseconds. +func (h *SubagentStopHookInput) UnmarshalJSON(data []byte) error { + type alias SubagentStopHookInput + aux := &struct { + Timestamp int64 `json:"timestamp"` + *alias + }{alias: (*alias)(h)} + if err := json.Unmarshal(data, aux); err != nil { + return err + } + h.Timestamp = time.UnixMilli(aux.Timestamp) + return nil +} + +// SubagentStopHookOutput can block a subagent's completion or replace its response. +// Decision must be "block" with a nonempty Reason, "allow", or empty to allow. +// Reason without a block decision and unsupported decisions fail the subagent. +type SubagentStopHookOutput struct { + Decision string `json:"decision,omitempty"` + Reason string `json:"reason,omitempty"` + ModifiedResponse *string `json:"modifiedResponse,omitempty"` +} + +// SubagentStopHandler handles subagent-stop hook invocations. +type SubagentStopHandler func(input SubagentStopHookInput, invocation HookInvocation) (*SubagentStopHookOutput, error) + // PreMCPToolCallHookInput is the input for a pre-mcp-tool-call hook type PreMCPToolCallHookInput struct { SessionID string `json:"sessionId"` @@ -1086,6 +1180,8 @@ type SessionHooks struct { OnSessionEnd SessionEndHandler OnErrorOccurred ErrorOccurredHandler OnAgentStop AgentStopHandler + OnSubagentStart SubagentStartHandler + OnSubagentStop SubagentStopHandler OnPreMCPToolCall PreMCPToolCallHandler } @@ -1242,6 +1338,8 @@ type ToolSearchConfig struct { type SessionFSCapabilities struct { // Sqlite indicates whether the provider supports SQLite query/exists operations. Sqlite bool + // Binary indicates whether the provider supports exact binary file reads and writes. + Binary bool } // SessionFSConfig configures a custom session filesystem provider. diff --git a/go/zsession_events.go b/go/zsession_events.go index d91f36537d..794a765fa8 100644 --- a/go/zsession_events.go +++ b/go/zsession_events.go @@ -143,6 +143,13 @@ type ( HookEndError = rpc.HookEndError HookProgressData = rpc.HookProgressData HookStartData = rpc.HookStartData + HumanResponseActor = rpc.HumanResponseActor + HumanResponseRecordedData = rpc.HumanResponseRecordedData + HumanResponseRecordedResponse = rpc.HumanResponseRecordedResponse + HumanResponseRecordedResponseAskUser = rpc.HumanResponseRecordedResponseAskUser + HumanResponseRecordedResponseExitPlanMode = rpc.HumanResponseRecordedResponseExitPlanMode + HumanResponseRecordedResponseResponseKind = rpc.HumanResponseRecordedResponseResponseKind + HumanResponseRecordedResponseUserInput = rpc.HumanResponseRecordedResponseUserInput IndexedSearchState = rpc.IndexedSearchState ManagedSettingsEnforcedAction = rpc.ManagedSettingsEnforcedAction ManagedSettingsEnforcedEscalation = rpc.ManagedSettingsEnforcedEscalation @@ -273,6 +280,7 @@ type ( PossibleURL = rpc.PossibleURL PromptCacheBreakData = rpc.PromptCacheBreakData RawCitationLocation = rpc.RawCitationLocation + RawHumanResponseRecordedResponse = rpc.RawHumanResponseRecordedResponse RawPermissionPromptRequest = rpc.RawPermissionPromptRequest RawPermissionRequest = rpc.RawPermissionRequest RawPermissionResult = rpc.RawPermissionResult @@ -422,6 +430,8 @@ type ( ToolExecutionCompleteContentType = rpc.ToolExecutionCompleteContentType ToolExecutionCompleteData = rpc.ToolExecutionCompleteData ToolExecutionCompleteError = rpc.ToolExecutionCompleteError + ToolExecutionCompleteFileEdit = rpc.ToolExecutionCompleteFileEdit + ToolExecutionCompleteFileEditKind = rpc.ToolExecutionCompleteFileEditKind ToolExecutionCompleteResult = rpc.ToolExecutionCompleteResult ToolExecutionCompleteShellExecution = rpc.ToolExecutionCompleteShellExecution ToolExecutionCompleteToolDescription = rpc.ToolExecutionCompleteToolDescription @@ -437,45 +447,48 @@ type ( ToolExecutionCompleteUIResourceMetaUIPermissionsClipboardWrite = rpc.ToolExecutionCompleteUIResourceMetaUIPermissionsClipboardWrite ToolExecutionCompleteUIResourceMetaUIPermissionsGeolocation = rpc.ToolExecutionCompleteUIResourceMetaUIPermissionsGeolocation ToolExecutionCompleteUIResourceMetaUIPermissionsMicrophone = rpc.ToolExecutionCompleteUIResourceMetaUIPermissionsMicrophone - ToolExecutionPartialResultData = rpc.ToolExecutionPartialResultData - ToolExecutionProgressData = rpc.ToolExecutionProgressData - ToolExecutionStartData = rpc.ToolExecutionStartData - ToolExecutionStartShellToolInfo = rpc.ToolExecutionStartShellToolInfo - ToolExecutionStartToolDescription = rpc.ToolExecutionStartToolDescription - ToolExecutionStartToolDescriptionMeta = rpc.ToolExecutionStartToolDescriptionMeta - ToolExecutionStartToolDescriptionMetaUI = rpc.ToolExecutionStartToolDescriptionMetaUI - ToolExecutionStartToolDescriptionMetaUIVisibility = rpc.ToolExecutionStartToolDescriptionMetaUIVisibility - ToolSearchActivatedData = rpc.ToolSearchActivatedData - ToolUserRequestedData = rpc.ToolUserRequestedData - UIEphemeralQueryData = rpc.UIEphemeralQueryData - UIEphemeralQueryPhase = rpc.UIEphemeralQueryPhase - UserInputCompletedData = rpc.UserInputCompletedData - UserInputRequestedData = rpc.UserInputRequestedData - UserMessageAgentMode = rpc.UserMessageAgentMode - UserMessageData = rpc.UserMessageData - UserMessageDelivery = rpc.UserMessageDelivery - UserToolSessionApproval = rpc.UserToolSessionApproval - UserToolSessionApprovalCommands = rpc.UserToolSessionApprovalCommands - UserToolSessionApprovalCustomTool = rpc.UserToolSessionApprovalCustomTool - UserToolSessionApprovalExtensionEnvAccess = rpc.UserToolSessionApprovalExtensionEnvAccess - UserToolSessionApprovalExtensionManagement = rpc.UserToolSessionApprovalExtensionManagement - UserToolSessionApprovalExtensionPermissionAccess = rpc.UserToolSessionApprovalExtensionPermissionAccess - UserToolSessionApprovalKind = rpc.UserToolSessionApprovalKind - UserToolSessionApprovalMCP = rpc.UserToolSessionApprovalMCP - UserToolSessionApprovalMemory = rpc.UserToolSessionApprovalMemory - UserToolSessionApprovalRead = rpc.UserToolSessionApprovalRead - UserToolSessionApprovalWorkflow = rpc.UserToolSessionApprovalWorkflow - UserToolSessionApprovalWrite = rpc.UserToolSessionApprovalWrite - Verbosity = rpc.Verbosity - WorkflowPermissionOperation = rpc.WorkflowPermissionOperation - WorkflowPermissionPhase = rpc.WorkflowPermissionPhase - WorkflowRunSettledData = rpc.WorkflowRunSettledData - WorkflowRunSettledStatus = rpc.WorkflowRunSettledStatus - WorkflowRunStartedData = rpc.WorkflowRunStartedData - WorkflowRunUpdatedData = rpc.WorkflowRunUpdatedData - WorkingDirectoryContext = rpc.WorkingDirectoryContext - WorkingDirectoryContextHostType = rpc.WorkingDirectoryContextHostType - WorkspaceFileChangedOperation = rpc.WorkspaceFileChangedOperation + // Deprecated: ToolExecutionPartialResultData is deprecated. + ToolExecutionPartialResultData = rpc.ToolExecutionPartialResultData + ToolExecutionProgressData = rpc.ToolExecutionProgressData + ToolExecutionStartData = rpc.ToolExecutionStartData + ToolExecutionStartShellToolInfo = rpc.ToolExecutionStartShellToolInfo + ToolExecutionStartToolDescription = rpc.ToolExecutionStartToolDescription + ToolExecutionStartToolDescriptionMeta = rpc.ToolExecutionStartToolDescriptionMeta + ToolExecutionStartToolDescriptionMetaUI = rpc.ToolExecutionStartToolDescriptionMetaUI + ToolExecutionStartToolDescriptionMetaUIVisibility = rpc.ToolExecutionStartToolDescriptionMetaUIVisibility + ToolSearchActivatedData = rpc.ToolSearchActivatedData + ToolShellOutputData = rpc.ToolShellOutputData + ToolShellOutputStream = rpc.ToolShellOutputStream + ToolUserRequestedData = rpc.ToolUserRequestedData + UIEphemeralQueryData = rpc.UIEphemeralQueryData + UIEphemeralQueryPhase = rpc.UIEphemeralQueryPhase + UserInputCompletedData = rpc.UserInputCompletedData + UserInputRequestedData = rpc.UserInputRequestedData + UserMessageAgentMode = rpc.UserMessageAgentMode + UserMessageData = rpc.UserMessageData + UserMessageDelivery = rpc.UserMessageDelivery + UserToolSessionApproval = rpc.UserToolSessionApproval + UserToolSessionApprovalCommands = rpc.UserToolSessionApprovalCommands + UserToolSessionApprovalCustomTool = rpc.UserToolSessionApprovalCustomTool + UserToolSessionApprovalExtensionEnvAccess = rpc.UserToolSessionApprovalExtensionEnvAccess + UserToolSessionApprovalExtensionManagement = rpc.UserToolSessionApprovalExtensionManagement + UserToolSessionApprovalExtensionPermissionAccess = rpc.UserToolSessionApprovalExtensionPermissionAccess + UserToolSessionApprovalKind = rpc.UserToolSessionApprovalKind + UserToolSessionApprovalMCP = rpc.UserToolSessionApprovalMCP + UserToolSessionApprovalMemory = rpc.UserToolSessionApprovalMemory + UserToolSessionApprovalRead = rpc.UserToolSessionApprovalRead + UserToolSessionApprovalWorkflow = rpc.UserToolSessionApprovalWorkflow + UserToolSessionApprovalWrite = rpc.UserToolSessionApprovalWrite + Verbosity = rpc.Verbosity + WorkflowPermissionOperation = rpc.WorkflowPermissionOperation + WorkflowPermissionPhase = rpc.WorkflowPermissionPhase + WorkflowRunSettledData = rpc.WorkflowRunSettledData + WorkflowRunSettledStatus = rpc.WorkflowRunSettledStatus + WorkflowRunStartedData = rpc.WorkflowRunStartedData + WorkflowRunUpdatedData = rpc.WorkflowRunUpdatedData + WorkingDirectoryContext = rpc.WorkingDirectoryContext + WorkingDirectoryContextHostType = rpc.WorkingDirectoryContextHostType + WorkspaceFileChangedOperation = rpc.WorkspaceFileChangedOperation ) // Session-event constants are generated in the rpc package and re-exported here for source compatibility. @@ -616,6 +629,12 @@ const ( FusionTurnKindUser = rpc.FusionTurnKindUser HandoffSourceTypeLocal = rpc.HandoffSourceTypeLocal HandoffSourceTypeRemote = rpc.HandoffSourceTypeRemote + HumanResponseActorHostAutomation = rpc.HumanResponseActorHostAutomation + HumanResponseActorHumanResponse = rpc.HumanResponseActorHumanResponse + HumanResponseActorUnknown = rpc.HumanResponseActorUnknown + HumanResponseRecordedResponseResponseKindAskUser = rpc.HumanResponseRecordedResponseResponseKindAskUser + HumanResponseRecordedResponseResponseKindExitPlanMode = rpc.HumanResponseRecordedResponseResponseKindExitPlanMode + HumanResponseRecordedResponseResponseKindUserInput = rpc.HumanResponseRecordedResponseResponseKindUserInput IndexedSearchStateDisabled = rpc.IndexedSearchStateDisabled IndexedSearchStateEnabled = rpc.IndexedSearchStateEnabled IndexedSearchStateFailed = rpc.IndexedSearchStateFailed @@ -878,6 +897,7 @@ const ( SessionEventTypeHookEnd = rpc.SessionEventTypeHookEnd SessionEventTypeHookProgress = rpc.SessionEventTypeHookProgress SessionEventTypeHookStart = rpc.SessionEventTypeHookStart + SessionEventTypeHumanResponseRecorded = rpc.SessionEventTypeHumanResponseRecorded SessionEventTypeMCPAppToolCallComplete = rpc.SessionEventTypeMCPAppToolCallComplete SessionEventTypeMCPHeadersRefreshCompleted = rpc.SessionEventTypeMCPHeadersRefreshCompleted SessionEventTypeMCPHeadersRefreshRequired = rpc.SessionEventTypeMCPHeadersRefreshRequired @@ -980,119 +1000,127 @@ const ( SessionEventTypeSystemMessage = rpc.SessionEventTypeSystemMessage SessionEventTypeSystemNotification = rpc.SessionEventTypeSystemNotification SessionEventTypeToolExecutionComplete = rpc.SessionEventTypeToolExecutionComplete - SessionEventTypeToolExecutionPartialResult = rpc.SessionEventTypeToolExecutionPartialResult - SessionEventTypeToolExecutionProgress = rpc.SessionEventTypeToolExecutionProgress - SessionEventTypeToolExecutionStart = rpc.SessionEventTypeToolExecutionStart - SessionEventTypeToolSearchActivated = rpc.SessionEventTypeToolSearchActivated - SessionEventTypeToolUserRequested = rpc.SessionEventTypeToolUserRequested - SessionEventTypeUIEphemeralQuery = rpc.SessionEventTypeUIEphemeralQuery - SessionEventTypeUserInputCompleted = rpc.SessionEventTypeUserInputCompleted - SessionEventTypeUserInputRequested = rpc.SessionEventTypeUserInputRequested - SessionEventTypeUserMessage = rpc.SessionEventTypeUserMessage - SessionEventTypeWorkflowRunSettled = rpc.SessionEventTypeWorkflowRunSettled - SessionEventTypeWorkflowRunStarted = rpc.SessionEventTypeWorkflowRunStarted - SessionEventTypeWorkflowRunUpdated = rpc.SessionEventTypeWorkflowRunUpdated - SessionLimitsExhaustedResponseActionAdd = rpc.SessionLimitsExhaustedResponseActionAdd - SessionLimitsExhaustedResponseActionCancel = rpc.SessionLimitsExhaustedResponseActionCancel - SessionLimitsExhaustedResponseActionSet = rpc.SessionLimitsExhaustedResponseActionSet - SessionLimitsExhaustedResponseActionUnset = rpc.SessionLimitsExhaustedResponseActionUnset - SessionModeAutopilot = rpc.SessionModeAutopilot - SessionModeInteractive = rpc.SessionModeInteractive - SessionModePlan = rpc.SessionModePlan - ShutdownTypeError = rpc.ShutdownTypeError - ShutdownTypeRoutine = rpc.ShutdownTypeRoutine - SkillInvokedTriggerAgentInvoked = rpc.SkillInvokedTriggerAgentInvoked - SkillInvokedTriggerContextLoad = rpc.SkillInvokedTriggerContextLoad - SkillInvokedTriggerUserInvoked = rpc.SkillInvokedTriggerUserInvoked - SkillSourceBuiltin = rpc.SkillSourceBuiltin - SkillSourceCustom = rpc.SkillSourceCustom - SkillSourceInherited = rpc.SkillSourceInherited - SkillSourcePersonalAgents = rpc.SkillSourcePersonalAgents - SkillSourcePersonalCopilot = rpc.SkillSourcePersonalCopilot - SkillSourcePlugin = rpc.SkillSourcePlugin - SkillSourceProject = rpc.SkillSourceProject - SkillSourceSDK = rpc.SkillSourceSDK - SubagentModelSelectionSourceAgentDefinitionDefault = rpc.SubagentModelSelectionSourceAgentDefinitionDefault - SubagentModelSelectionSourceComplementaryDefault = rpc.SubagentModelSelectionSourceComplementaryDefault - SubagentModelSelectionSourceConfiguredPreference = rpc.SubagentModelSelectionSourceConfiguredPreference - SubagentModelSelectionSourceConfiguredRequired = rpc.SubagentModelSelectionSourceConfiguredRequired - SubagentModelSelectionSourceExplicitOverride = rpc.SubagentModelSelectionSourceExplicitOverride - SubagentModelSelectionSourceRuntimePolicy = rpc.SubagentModelSelectionSourceRuntimePolicy - SubagentModelSelectionSourceSessionInheritance = rpc.SubagentModelSelectionSourceSessionInheritance - SubagentTaskModelSourceCustomAgentDefinition = rpc.SubagentTaskModelSourceCustomAgentDefinition - SubagentTaskModelSourceSubagentConfiguration = rpc.SubagentTaskModelSourceSubagentConfiguration - SubagentTaskModelSourceTaskArgument = rpc.SubagentTaskModelSourceTaskArgument - SubagentTaskModelSourceUnset = rpc.SubagentTaskModelSourceUnset - SystemMessageRoleDeveloper = rpc.SystemMessageRoleDeveloper - SystemMessageRoleSystem = rpc.SystemMessageRoleSystem - SystemNotificationAgentCompletedStatusCompleted = rpc.SystemNotificationAgentCompletedStatusCompleted - SystemNotificationAgentCompletedStatusFailed = rpc.SystemNotificationAgentCompletedStatusFailed - SystemNotificationTypeAgentCompleted = rpc.SystemNotificationTypeAgentCompleted - SystemNotificationTypeAgentIdle = rpc.SystemNotificationTypeAgentIdle - SystemNotificationTypeInstructionDiscovered = rpc.SystemNotificationTypeInstructionDiscovered - SystemNotificationTypeNewInboxMessage = rpc.SystemNotificationTypeNewInboxMessage - SystemNotificationTypeShellCompleted = rpc.SystemNotificationTypeShellCompleted - SystemNotificationTypeShellDetachedCompleted = rpc.SystemNotificationTypeShellDetachedCompleted - SystemNotificationTypeUnclassified = rpc.SystemNotificationTypeUnclassified - SystemNotificationTypeWorkflowCompleted = rpc.SystemNotificationTypeWorkflowCompleted - SystemNotificationWorkflowCompletedStatusCancelled = rpc.SystemNotificationWorkflowCompletedStatusCancelled - SystemNotificationWorkflowCompletedStatusCompleted = rpc.SystemNotificationWorkflowCompletedStatusCompleted - SystemNotificationWorkflowCompletedStatusError = rpc.SystemNotificationWorkflowCompletedStatusError - SystemNotificationWorkflowCompletedStatusHalted = rpc.SystemNotificationWorkflowCompletedStatusHalted - SystemNotificationWorkflowCompletedStatusPaused = rpc.SystemNotificationWorkflowCompletedStatusPaused - SystemNotificationWorkflowPauseInfoTypeCheckpoint = rpc.SystemNotificationWorkflowPauseInfoTypeCheckpoint - SystemNotificationWorkflowPauseInfoTypeUser = rpc.SystemNotificationWorkflowPauseInfoTypeUser - TaskBlockerKindPermissionRecovery = rpc.TaskBlockerKindPermissionRecovery - TaskCompletionOutcomeBlocked = rpc.TaskCompletionOutcomeBlocked - TaskCompletionOutcomeCompleted = rpc.TaskCompletionOutcomeCompleted - TaskCompletionOutcomeContinue = rpc.TaskCompletionOutcomeContinue - ToolExecutionCompleteContentResourceLinkIconThemeDark = rpc.ToolExecutionCompleteContentResourceLinkIconThemeDark - ToolExecutionCompleteContentResourceLinkIconThemeLight = rpc.ToolExecutionCompleteContentResourceLinkIconThemeLight - ToolExecutionCompleteContentTypeAudio = rpc.ToolExecutionCompleteContentTypeAudio - ToolExecutionCompleteContentTypeImage = rpc.ToolExecutionCompleteContentTypeImage - ToolExecutionCompleteContentTypeResource = rpc.ToolExecutionCompleteContentTypeResource - ToolExecutionCompleteContentTypeResourceLink = rpc.ToolExecutionCompleteContentTypeResourceLink - ToolExecutionCompleteContentTypeShellExit = rpc.ToolExecutionCompleteContentTypeShellExit - ToolExecutionCompleteContentTypeTerminal = rpc.ToolExecutionCompleteContentTypeTerminal - ToolExecutionCompleteContentTypeText = rpc.ToolExecutionCompleteContentTypeText - ToolExecutionCompleteToolDescriptionMetaUIVisibilityApp = rpc.ToolExecutionCompleteToolDescriptionMetaUIVisibilityApp - ToolExecutionCompleteToolDescriptionMetaUIVisibilityModel = rpc.ToolExecutionCompleteToolDescriptionMetaUIVisibilityModel - ToolExecutionStartToolDescriptionMetaUIVisibilityApp = rpc.ToolExecutionStartToolDescriptionMetaUIVisibilityApp - ToolExecutionStartToolDescriptionMetaUIVisibilityModel = rpc.ToolExecutionStartToolDescriptionMetaUIVisibilityModel - UIEphemeralQueryPhaseAborted = rpc.UIEphemeralQueryPhaseAborted - UIEphemeralQueryPhaseChunk = rpc.UIEphemeralQueryPhaseChunk - UIEphemeralQueryPhaseCompleted = rpc.UIEphemeralQueryPhaseCompleted - UIEphemeralQueryPhaseFailed = rpc.UIEphemeralQueryPhaseFailed - UIEphemeralQueryPhaseStarted = rpc.UIEphemeralQueryPhaseStarted - UserMessageAgentModeAutopilot = rpc.UserMessageAgentModeAutopilot - UserMessageAgentModeInteractive = rpc.UserMessageAgentModeInteractive - UserMessageAgentModePlan = rpc.UserMessageAgentModePlan - UserMessageAgentModeShell = rpc.UserMessageAgentModeShell - UserMessageDeliveryIdle = rpc.UserMessageDeliveryIdle - UserMessageDeliveryQueued = rpc.UserMessageDeliveryQueued - UserMessageDeliverySteering = rpc.UserMessageDeliverySteering - UserToolSessionApprovalKindCommands = rpc.UserToolSessionApprovalKindCommands - UserToolSessionApprovalKindCustomTool = rpc.UserToolSessionApprovalKindCustomTool - UserToolSessionApprovalKindExtensionEnvAccess = rpc.UserToolSessionApprovalKindExtensionEnvAccess - UserToolSessionApprovalKindExtensionManagement = rpc.UserToolSessionApprovalKindExtensionManagement - UserToolSessionApprovalKindExtensionPermissionAccess = rpc.UserToolSessionApprovalKindExtensionPermissionAccess - UserToolSessionApprovalKindMCP = rpc.UserToolSessionApprovalKindMCP - UserToolSessionApprovalKindMemory = rpc.UserToolSessionApprovalKindMemory - UserToolSessionApprovalKindRead = rpc.UserToolSessionApprovalKindRead - UserToolSessionApprovalKindWorkflow = rpc.UserToolSessionApprovalKindWorkflow - UserToolSessionApprovalKindWrite = rpc.UserToolSessionApprovalKindWrite - VerbosityHigh = rpc.VerbosityHigh - VerbosityLow = rpc.VerbosityLow - VerbosityMedium = rpc.VerbosityMedium - WorkflowPermissionOperationAuthor = rpc.WorkflowPermissionOperationAuthor - WorkflowPermissionOperationRun = rpc.WorkflowPermissionOperationRun - WorkflowRunSettledStatusCancelled = rpc.WorkflowRunSettledStatusCancelled - WorkflowRunSettledStatusCompleted = rpc.WorkflowRunSettledStatusCompleted - WorkflowRunSettledStatusError = rpc.WorkflowRunSettledStatusError - WorkflowRunSettledStatusHalted = rpc.WorkflowRunSettledStatusHalted - WorkflowRunSettledStatusPaused = rpc.WorkflowRunSettledStatusPaused - WorkingDirectoryContextHostTypeADO = rpc.WorkingDirectoryContextHostTypeADO - WorkingDirectoryContextHostTypeGitHub = rpc.WorkingDirectoryContextHostTypeGitHub - WorkspaceFileChangedOperationCreate = rpc.WorkspaceFileChangedOperationCreate - WorkspaceFileChangedOperationUpdate = rpc.WorkspaceFileChangedOperationUpdate + // Deprecated: SessionEventTypeToolExecutionPartialResult identifies a deprecated event. + SessionEventTypeToolExecutionPartialResult = rpc.SessionEventTypeToolExecutionPartialResult + SessionEventTypeToolExecutionProgress = rpc.SessionEventTypeToolExecutionProgress + SessionEventTypeToolExecutionStart = rpc.SessionEventTypeToolExecutionStart + SessionEventTypeToolSearchActivated = rpc.SessionEventTypeToolSearchActivated + SessionEventTypeToolShellOutput = rpc.SessionEventTypeToolShellOutput + SessionEventTypeToolUserRequested = rpc.SessionEventTypeToolUserRequested + SessionEventTypeUIEphemeralQuery = rpc.SessionEventTypeUIEphemeralQuery + SessionEventTypeUserInputCompleted = rpc.SessionEventTypeUserInputCompleted + SessionEventTypeUserInputRequested = rpc.SessionEventTypeUserInputRequested + SessionEventTypeUserMessage = rpc.SessionEventTypeUserMessage + SessionEventTypeWorkflowRunSettled = rpc.SessionEventTypeWorkflowRunSettled + SessionEventTypeWorkflowRunStarted = rpc.SessionEventTypeWorkflowRunStarted + SessionEventTypeWorkflowRunUpdated = rpc.SessionEventTypeWorkflowRunUpdated + SessionLimitsExhaustedResponseActionAdd = rpc.SessionLimitsExhaustedResponseActionAdd + SessionLimitsExhaustedResponseActionCancel = rpc.SessionLimitsExhaustedResponseActionCancel + SessionLimitsExhaustedResponseActionSet = rpc.SessionLimitsExhaustedResponseActionSet + SessionLimitsExhaustedResponseActionUnset = rpc.SessionLimitsExhaustedResponseActionUnset + SessionModeAutopilot = rpc.SessionModeAutopilot + SessionModeInteractive = rpc.SessionModeInteractive + SessionModePlan = rpc.SessionModePlan + ShutdownTypeError = rpc.ShutdownTypeError + ShutdownTypeRoutine = rpc.ShutdownTypeRoutine + SkillInvokedTriggerAgentInvoked = rpc.SkillInvokedTriggerAgentInvoked + SkillInvokedTriggerContextLoad = rpc.SkillInvokedTriggerContextLoad + SkillInvokedTriggerUserInvoked = rpc.SkillInvokedTriggerUserInvoked + SkillSourceBuiltin = rpc.SkillSourceBuiltin + SkillSourceCustom = rpc.SkillSourceCustom + SkillSourceInherited = rpc.SkillSourceInherited + SkillSourcePersonalAgents = rpc.SkillSourcePersonalAgents + SkillSourcePersonalCopilot = rpc.SkillSourcePersonalCopilot + SkillSourcePlugin = rpc.SkillSourcePlugin + SkillSourceProject = rpc.SkillSourceProject + SkillSourceSDK = rpc.SkillSourceSDK + SubagentModelSelectionSourceAgentDefinitionDefault = rpc.SubagentModelSelectionSourceAgentDefinitionDefault + SubagentModelSelectionSourceComplementaryDefault = rpc.SubagentModelSelectionSourceComplementaryDefault + SubagentModelSelectionSourceConfiguredPreference = rpc.SubagentModelSelectionSourceConfiguredPreference + SubagentModelSelectionSourceConfiguredRequired = rpc.SubagentModelSelectionSourceConfiguredRequired + SubagentModelSelectionSourceExplicitOverride = rpc.SubagentModelSelectionSourceExplicitOverride + SubagentModelSelectionSourceRuntimePolicy = rpc.SubagentModelSelectionSourceRuntimePolicy + SubagentModelSelectionSourceSessionInheritance = rpc.SubagentModelSelectionSourceSessionInheritance + SubagentTaskModelSourceCustomAgentDefinition = rpc.SubagentTaskModelSourceCustomAgentDefinition + SubagentTaskModelSourceSubagentConfiguration = rpc.SubagentTaskModelSourceSubagentConfiguration + SubagentTaskModelSourceTaskArgument = rpc.SubagentTaskModelSourceTaskArgument + SubagentTaskModelSourceUnset = rpc.SubagentTaskModelSourceUnset + SystemMessageRoleDeveloper = rpc.SystemMessageRoleDeveloper + SystemMessageRoleSystem = rpc.SystemMessageRoleSystem + SystemNotificationAgentCompletedStatusCompleted = rpc.SystemNotificationAgentCompletedStatusCompleted + SystemNotificationAgentCompletedStatusFailed = rpc.SystemNotificationAgentCompletedStatusFailed + SystemNotificationTypeAgentCompleted = rpc.SystemNotificationTypeAgentCompleted + SystemNotificationTypeAgentIdle = rpc.SystemNotificationTypeAgentIdle + SystemNotificationTypeInstructionDiscovered = rpc.SystemNotificationTypeInstructionDiscovered + SystemNotificationTypeNewInboxMessage = rpc.SystemNotificationTypeNewInboxMessage + SystemNotificationTypeShellCompleted = rpc.SystemNotificationTypeShellCompleted + SystemNotificationTypeShellDetachedCompleted = rpc.SystemNotificationTypeShellDetachedCompleted + SystemNotificationTypeUnclassified = rpc.SystemNotificationTypeUnclassified + SystemNotificationTypeWorkflowCompleted = rpc.SystemNotificationTypeWorkflowCompleted + SystemNotificationWorkflowCompletedStatusCancelled = rpc.SystemNotificationWorkflowCompletedStatusCancelled + SystemNotificationWorkflowCompletedStatusCompleted = rpc.SystemNotificationWorkflowCompletedStatusCompleted + SystemNotificationWorkflowCompletedStatusError = rpc.SystemNotificationWorkflowCompletedStatusError + SystemNotificationWorkflowCompletedStatusHalted = rpc.SystemNotificationWorkflowCompletedStatusHalted + SystemNotificationWorkflowCompletedStatusPaused = rpc.SystemNotificationWorkflowCompletedStatusPaused + SystemNotificationWorkflowPauseInfoTypeCheckpoint = rpc.SystemNotificationWorkflowPauseInfoTypeCheckpoint + SystemNotificationWorkflowPauseInfoTypeUser = rpc.SystemNotificationWorkflowPauseInfoTypeUser + TaskBlockerKindPermissionRecovery = rpc.TaskBlockerKindPermissionRecovery + TaskCompletionOutcomeBlocked = rpc.TaskCompletionOutcomeBlocked + TaskCompletionOutcomeCompleted = rpc.TaskCompletionOutcomeCompleted + TaskCompletionOutcomeContinue = rpc.TaskCompletionOutcomeContinue + ToolExecutionCompleteContentResourceLinkIconThemeDark = rpc.ToolExecutionCompleteContentResourceLinkIconThemeDark + ToolExecutionCompleteContentResourceLinkIconThemeLight = rpc.ToolExecutionCompleteContentResourceLinkIconThemeLight + ToolExecutionCompleteContentTypeAudio = rpc.ToolExecutionCompleteContentTypeAudio + ToolExecutionCompleteContentTypeImage = rpc.ToolExecutionCompleteContentTypeImage + ToolExecutionCompleteContentTypeResource = rpc.ToolExecutionCompleteContentTypeResource + ToolExecutionCompleteContentTypeResourceLink = rpc.ToolExecutionCompleteContentTypeResourceLink + ToolExecutionCompleteContentTypeShellExit = rpc.ToolExecutionCompleteContentTypeShellExit + ToolExecutionCompleteContentTypeTerminal = rpc.ToolExecutionCompleteContentTypeTerminal + ToolExecutionCompleteContentTypeText = rpc.ToolExecutionCompleteContentTypeText + ToolExecutionCompleteFileEditKindCreate = rpc.ToolExecutionCompleteFileEditKindCreate + ToolExecutionCompleteFileEditKindDelete = rpc.ToolExecutionCompleteFileEditKindDelete + ToolExecutionCompleteFileEditKindEdit = rpc.ToolExecutionCompleteFileEditKindEdit + ToolExecutionCompleteToolDescriptionMetaUIVisibilityApp = rpc.ToolExecutionCompleteToolDescriptionMetaUIVisibilityApp + ToolExecutionCompleteToolDescriptionMetaUIVisibilityModel = rpc.ToolExecutionCompleteToolDescriptionMetaUIVisibilityModel + ToolExecutionStartToolDescriptionMetaUIVisibilityApp = rpc.ToolExecutionStartToolDescriptionMetaUIVisibilityApp + ToolExecutionStartToolDescriptionMetaUIVisibilityModel = rpc.ToolExecutionStartToolDescriptionMetaUIVisibilityModel + ToolShellOutputStreamStderr = rpc.ToolShellOutputStreamStderr + ToolShellOutputStreamStdout = rpc.ToolShellOutputStreamStdout + ToolShellOutputStreamTerminal = rpc.ToolShellOutputStreamTerminal + UIEphemeralQueryPhaseAborted = rpc.UIEphemeralQueryPhaseAborted + UIEphemeralQueryPhaseChunk = rpc.UIEphemeralQueryPhaseChunk + UIEphemeralQueryPhaseCompleted = rpc.UIEphemeralQueryPhaseCompleted + UIEphemeralQueryPhaseFailed = rpc.UIEphemeralQueryPhaseFailed + UIEphemeralQueryPhaseStarted = rpc.UIEphemeralQueryPhaseStarted + UserMessageAgentModeAutopilot = rpc.UserMessageAgentModeAutopilot + UserMessageAgentModeInteractive = rpc.UserMessageAgentModeInteractive + UserMessageAgentModePlan = rpc.UserMessageAgentModePlan + UserMessageAgentModeShell = rpc.UserMessageAgentModeShell + UserMessageDeliveryIdle = rpc.UserMessageDeliveryIdle + UserMessageDeliveryQueued = rpc.UserMessageDeliveryQueued + UserMessageDeliverySteering = rpc.UserMessageDeliverySteering + UserToolSessionApprovalKindCommands = rpc.UserToolSessionApprovalKindCommands + UserToolSessionApprovalKindCustomTool = rpc.UserToolSessionApprovalKindCustomTool + UserToolSessionApprovalKindExtensionEnvAccess = rpc.UserToolSessionApprovalKindExtensionEnvAccess + UserToolSessionApprovalKindExtensionManagement = rpc.UserToolSessionApprovalKindExtensionManagement + UserToolSessionApprovalKindExtensionPermissionAccess = rpc.UserToolSessionApprovalKindExtensionPermissionAccess + UserToolSessionApprovalKindMCP = rpc.UserToolSessionApprovalKindMCP + UserToolSessionApprovalKindMemory = rpc.UserToolSessionApprovalKindMemory + UserToolSessionApprovalKindRead = rpc.UserToolSessionApprovalKindRead + UserToolSessionApprovalKindWorkflow = rpc.UserToolSessionApprovalKindWorkflow + UserToolSessionApprovalKindWrite = rpc.UserToolSessionApprovalKindWrite + VerbosityHigh = rpc.VerbosityHigh + VerbosityLow = rpc.VerbosityLow + VerbosityMedium = rpc.VerbosityMedium + WorkflowPermissionOperationAuthor = rpc.WorkflowPermissionOperationAuthor + WorkflowPermissionOperationRun = rpc.WorkflowPermissionOperationRun + WorkflowRunSettledStatusCancelled = rpc.WorkflowRunSettledStatusCancelled + WorkflowRunSettledStatusCompleted = rpc.WorkflowRunSettledStatusCompleted + WorkflowRunSettledStatusError = rpc.WorkflowRunSettledStatusError + WorkflowRunSettledStatusHalted = rpc.WorkflowRunSettledStatusHalted + WorkflowRunSettledStatusPaused = rpc.WorkflowRunSettledStatusPaused + WorkingDirectoryContextHostTypeADO = rpc.WorkingDirectoryContextHostTypeADO + WorkingDirectoryContextHostTypeGitHub = rpc.WorkingDirectoryContextHostTypeGitHub + WorkspaceFileChangedOperationCreate = rpc.WorkspaceFileChangedOperationCreate + WorkspaceFileChangedOperationUpdate = rpc.WorkspaceFileChangedOperationUpdate ) diff --git a/java/AGENTS.md b/java/AGENTS.md index 684dcdb541..45b9fb9709 100644 --- a/java/AGENTS.md +++ b/java/AGENTS.md @@ -66,7 +66,7 @@ Add new Java E2E tests for Java SDK surface behavior, not shared runtime functionality; new SDK-accessible runtime E2Es belong in `../nodejs/test/e2e/`. For replay-backed Java integration tests, use the on-demand [`sdk-java-e2e-test` skill](../.github/skills/sdk-java-e2e-test/SKILL.md). -Its snapshot workflow and companion examples are not required for unrelated +Its snapshot workflow is not required for unrelated Java edits. For JDK 17 compatibility testing, run the JDK 25-built artifact on JDK 17 without recompiling it, following [Development Setup](README.md#development-setup). diff --git a/java/README.md b/java/README.md index ffb80d3dc0..cc2c5cf3a4 100644 --- a/java/README.md +++ b/java/README.md @@ -15,6 +15,8 @@ Java SDK for programmatic control of GitHub Copilot CLI, enabling you to build AI-powered applications and agentic workflows. The Java SDK tracks the official GitHub Copilot SDK family (TypeScript, Python, Go, .NET, and Rust). +The generated `SessionFsSetProviderCapabilities.binary` field describes the runtime protocol, not a Java provider API. The Java SDK cannot currently register a session filesystem provider or handle `sessionFs.readFileBytes` or `sessionFs.writeFileBytes`; do not advertise binary operations from Java. Java provider and binary support is deferred until the SDK exposes provider registration and reverse-RPC dispatch. Provider-only images currently require a provider-capable SDK. + ## Prerequisites To use the SDK, you'll need: @@ -190,6 +192,37 @@ directly. `CopilotClientOptions.setCwd(...)` sets the runtime process working directory, which otherwise inherits the current process working directory. `SessionConfig.setWorkingDirectory(...)` sets the session working directory, which otherwise defaults to the runtime process working directory. +### Subagent lifecycle hooks + +Register `setOnSubagentStart` and `setOnSubagentStop` on `SessionHooks` to inspect +the parent session and subagent metadata around each child run: + +```java +import com.github.copilot.rpc.SessionConfig; +import com.github.copilot.rpc.SessionHooks; +import com.github.copilot.rpc.SubagentStartHookOutput; +import com.github.copilot.rpc.SubagentStopHookOutput; +import java.util.concurrent.CompletableFuture; + +var hooks = new SessionHooks() + .setOnSubagentStart((input, invocation) -> + CompletableFuture.completedFuture( + new SubagentStartHookOutput("Check the relevant tests first."))) + .setOnSubagentStop((input, invocation) -> + CompletableFuture.completedFuture( + new SubagentStopHookOutput(null, null, input.response()))); +var config = new SessionConfig().setHooks(hooks); +``` + +`SubagentStartHookInput` includes the parent `sessionId`, `timestamp`, `cwd`, +`transcriptPath`, `agentName`, and optional display name and description. Its +`additionalContext` output is prepended to the child's first prompt. +`SubagentStopHookInput` also includes `agentType`, optional `agentId`, +`stopReason`, and the last assistant `response`. Return a stop output with +`decision` set to `"block"` and a nonempty `reason` to request another child +turn, or set `modifiedResponse` to replace the final response. A block takes +precedence over a rewrite. + `CopilotClientOptions.setExtensionLaunchProvider(...)` configures an experimental connection-level resolver for extension launch profiles. The client installs the reverse-RPC handler and registers the provider during startup before sessions can @@ -265,6 +298,12 @@ with the other SDKs. No other generated type changes. | `McpInstallPlan.transportChoices()` | `List` | `List` (`McpPlanTransportChoicePackage` / `McpPlanTransportChoiceRemote`, by `installMethod`) | | `McpInstallationManagementResultOutcome.getOutcome()` / `setOutcome(...)` | `Object` | `McpInstallationManagementOutcome`, whose operation variant carries `McpInstallationOperationStatus` (by `phase`) | +## System Message Customization + +Use `SessionConfig.setSystemMessage(...)` with a `SystemMessageConfig` in `SystemMessageMode.CUSTOMIZE` to override individual prompt sections. Section keys are defined by `SystemMessageSections`; use `SectionOverride.setAction(...)` for static overrides or `SectionOverride.setTransform(...)` for a transform callback. + +`SystemMessageSections.LAST_INSTRUCTIONS` (`last_instructions`) includes configured subagent-model guidance when the `task` tool is available. Removing or replacing this section also removes that guidance; a transform callback receives the complete section, including the guidance, and its returned content is authoritative. Append, prepend, and preserve retain their usual section semantics. These overrides change prompt prose only, not configured subagent models, tool availability, or runtime dispatch policy. `SystemMessageSections.RUNTIME_INSTRUCTIONS` is a separate section: removing it does not remove `SystemMessageSections.LAST_INSTRUCTIONS`. + ## Message source Use `MessageSource.SYSTEM` for application-generated system context and @@ -477,6 +516,31 @@ Chain fluent modifiers to set tool options: For design context and decision rationale, see [ADR-006](docs/adr/adr-006-tool-definition-inline.md). +### String-schema `apply_patch` overrides + +An explicit `apply_patch` override can declare `Map.of("type", "string")`. +The model sees a required `input` property, but the runtime restores the scalar +patch text before dispatch. Use `ToolInvocation.getArgumentsAs(String.class)` +to read it; `getArguments()` is for object-shaped arguments. This example +returns trimmed patch text; replace the handler body with your own patch +implementation: + +```java +import java.util.Map; +import java.util.concurrent.CompletableFuture; +import com.github.copilot.rpc.ToolDefinition; + +ToolDefinition applyPatch = ToolDefinition.create("apply_patch", "Apply a patch", Map.of("type", "string"), + invocation -> { + String patch = invocation.getArgumentsAs(String.class); + return CompletableFuture.completedFuture(patch.trim()); + }).overridesBuiltInTool(true); +``` + +Register this definition with `SessionConfig.setTools(...)`. String-schema +`apply_patch` overrides cannot contain JSON Schema references; use an object +schema if references are needed. + ## Changing tools on a live session (experimental) `CopilotSession.setTools(List)` replaces the external tools diff --git a/java/copilot-native/scripts/fetch-native.mjs b/java/copilot-native/scripts/fetch-native.mjs index a794c69d9a..e82371ab2c 100644 --- a/java/copilot-native/scripts/fetch-native.mjs +++ b/java/copilot-native/scripts/fetch-native.mjs @@ -28,6 +28,7 @@ const excludedTopLevel = new Set([ 'app.js', 'assets', 'changelog.json', + 'cli-main.js', 'copilot', 'copilot.exe', 'foundry-local-sdk', diff --git a/java/copilot-native/scripts/fetch-native.test.mjs b/java/copilot-native/scripts/fetch-native.test.mjs index 050275e79c..e8ee4f704d 100644 --- a/java/copilot-native/scripts/fetch-native.test.mjs +++ b/java/copilot-native/scripts/fetch-native.test.mjs @@ -130,6 +130,7 @@ test('stages retained package assets with a bounded number of archive passes', ( fs.writeFileSync(path.join(packageRoot, 'definitions', `${i}.json`), `${i}`); } fs.writeFileSync(path.join(packageRoot, 'app.js'), 'excluded'); + fs.writeFileSync(path.join(packageRoot, 'cli-main.js'), 'excluded'); fs.writeFileSync(path.join(packageRoot, 'LICENSE.md'), 'excluded'); fs.writeFileSync(path.join(packageRoot, 'README.md'), 'excluded'); const tarball = path.join(fixture.repoRoot, 'fixture.tgz'); @@ -178,6 +179,7 @@ test('stages retained package assets with a bounded number of archive passes', ( assert.equal(fs.readFileSync(fixture.runtimePath, 'utf8'), runtimeContent); assert.equal(fs.readFileSync(fixture.wrapperPath, 'utf8'), wrapperContent); assert.equal(fs.existsSync(path.join(resourceDir, 'app.js')), false); + assert.equal(fs.existsSync(path.join(resourceDir, 'cli-main.js')), false); assert.equal(fs.existsSync(path.join(resourceDir, 'copilot')), false); assert.equal(fs.existsSync(path.join(resourceDir, 'LICENSE.md')), false); assert.equal(fs.existsSync(path.join(resourceDir, 'README.md')), false); diff --git a/java/scripts/codegen/java.test.ts b/java/scripts/codegen/java.test.ts index 294b8ed27f..e489eef017 100644 --- a/java/scripts/codegen/java.test.ts +++ b/java/scripts/codegen/java.test.ts @@ -10,8 +10,10 @@ import test from "node:test"; import type { JSONSchema7 } from "json-schema"; import { + collectDirectDiscriminatedUnionTypeNames, collectNestedDiscriminatedUnionTypeNames, generateApiMethod, + extractEventVariants, generateRpcClass, isMainModule, renderEventVariantClass, @@ -30,6 +32,65 @@ test("arbitrary handoff maps accept scalar and structured JSON values", () => { assert.ok(result.imports.has("java.util.Map")); }); +test("preserves whole-event deprecation through a referenced event envelope", () => { + const schema: JSONSchema7 = { + definitions: { + SessionEvent: { anyOf: [{ $ref: "#/definitions/LegacyEvent" }] }, + LegacyEvent: { + type: "object", + deprecated: true, + properties: { + type: { const: "tool.execution_partial_result" }, + data: { $ref: "#/definitions/LegacyData" }, + }, + } as JSONSchema7, + LegacyData: { type: "object", properties: { partialOutput: { type: "string" } } }, + }, + }; + const variants = extractEventVariants(schema); + assert.equal(variants.length, 1); + assert.equal(variants[0].deprecated, true); + assert.match(renderEventVariantClass(variants[0], "com.github.copilot.generated"), /@Deprecated/); +}); + +for (const referencedData of [false, true]) { + for (const eventDeprecated of [false, true]) { + for (const dataDeprecated of [false, true]) { + test(`preserves independent payload deprecation (referenced=${referencedData}, event=${eventDeprecated}, data=${dataDeprecated})`, () => { + const dataSchema: JSONSchema7 & { deprecated: boolean } = { + type: "object", + deprecated: dataDeprecated, + properties: { partialOutput: { type: "string" } }, + }; + const eventSchema: JSONSchema7 & { deprecated: boolean } = { + type: "object", + deprecated: eventDeprecated, + properties: { + type: { const: "tool.execution_partial_result" }, + data: referencedData ? { $ref: "#/definitions/LegacyData" } : dataSchema, + }, + }; + const schema: JSONSchema7 = { + definitions: { + SessionEvent: { anyOf: [{ $ref: "#/definitions/LegacyEvent" }] }, + LegacyEvent: eventSchema, + LegacyData: dataSchema, + }, + }; + const variants = extractEventVariants(schema); + assert.equal(variants.length, 1); + const source = renderEventVariantClass(variants[0], "com.github.copilot.generated"); + const eventAnnotation = /@Deprecated\n(?:@[^\n]*\n)*public final class ToolExecutionPartialResultEvent\b/; + const dataAnnotation = / @Deprecated\n(?: @[^\n]*\n)* public record ToolExecutionPartialResultEventData\b/; + if (eventDeprecated) assert.match(source, eventAnnotation); + else assert.doesNotMatch(source, eventAnnotation); + if (dataDeprecated) assert.match(source, dataAnnotation); + else assert.doesNotMatch(source, dataAnnotation); + }); + } + } +} + test("preserves diagnostics configuration for session create and resume", async () => { const files = await renderRpcTypes({ definitions: { @@ -50,6 +111,56 @@ test("preserves diagnostics configuration for session create and resume", async assert.ok([...files.keys()].some((file) => file.endsWith("/DiagnosticSourcesConfiguration.java"))); }); +test("customization reload types are experimental without opening unrelated types", async () => { + const files = await renderRpcTypes({ + definitions: { + CustomizationReloadStatus: { type: "string", enum: ["reloaded", "failed"] }, + CustomizationReloadSubsystem: { type: "string", enum: ["skills", "hooks"] }, + CustomizationReloadOutcome: { + type: "object", + description: "Result of one customization reload component.", + properties: { status: { $ref: "#/definitions/CustomizationReloadStatus" } }, + }, + ClosedStatus: { type: "string", enum: ["ready"] }, + ExampleResult: { + type: "object", + properties: { + status: { $ref: "#/definitions/CustomizationReloadStatus" }, + subsystem: { $ref: "#/definitions/CustomizationReloadSubsystem" }, + outcome: { $ref: "#/definitions/CustomizationReloadOutcome" }, + closed: { $ref: "#/definitions/ClosedStatus" }, + }, + }, + }, + server: { + example: { + rpcMethod: "example", + params: null, + result: { $ref: "#/definitions/ExampleResult" }, + }, + }, + }, {}); + for (const name of ["CustomizationReloadStatus", "CustomizationReloadSubsystem"]) { + const source = [...files].find(([file]) => file.endsWith(`/${name}.java`))?.[1]; + assert.ok(source, `missing ${name}.java`); + assert.match(source, /public static final \w+ UNKNOWN = new \w+\("unknown"\);/); + assert.match(source, /if \(value != null\) return new \w+\(value\);/); + assert.match(source, /@com\.fasterxml\.jackson\.annotation\.JsonValue/); + assert.match(source, /import com\.github\.copilot\.CopilotExperimental;/); + assert.match(source, /@apiNote This type is experimental and may change in a future version\./); + assert.match(source, /@CopilotExperimental/); + } + const outcome = [...files].find(([file]) => file.endsWith("/CustomizationReloadOutcome.java"))?.[1]; + assert.ok(outcome, "missing CustomizationReloadOutcome.java"); + assert.match(outcome, /import com\.github\.copilot\.CopilotExperimental;/); + assert.match(outcome, /@apiNote This type is experimental and may change in a future version\./); + assert.match(outcome, /@CopilotExperimental/); + const closed = [...files].find(([file]) => file.endsWith("/ClosedStatus.java"))?.[1]; + assert.ok(closed); + assert.doesNotMatch(closed, /return UNKNOWN/); + assert.doesNotMatch(closed, /@CopilotExperimental/); +}); + test("recognizes an entrypoint reached through a linked directory", (t) => { const root = fs.mkdtempSync(path.join(os.tmpdir(), "copilot-java-codegen-entrypoint-")); t.after(() => fs.rmSync(root, { recursive: true, force: true })); @@ -155,6 +266,59 @@ test("ordinary and empty root objects retain their existing records", () => { assert.doesNotMatch(empty, /JsonNode|DELEGATING|JsonValue/); }); +test("session event fields promote directly referenced discriminated response unions", () => { + const definitions: Record = { + TypedResponse: { + anyOf: [ + { + type: "object", + properties: { + responseKind: { const: "answer" }, + answer: { type: "string" }, + }, + required: ["responseKind", "answer"], + }, + { + type: "object", + properties: { + responseKind: { const: "decision" }, + approved: { type: "boolean" }, + }, + required: ["responseKind", "approved"], + }, + ], + }, + }; + const dataSchema: JSONSchema7 = { + type: "object", + properties: { + response: { $ref: "#/definitions/TypedResponse" }, + }, + }; + const promoted = collectDirectDiscriminatedUnionTypeNames( + dataSchema, + definitions, + new Set(["TypedResponse"]), + ); + const standaloneTypes = new Map(); + + const result = schemaTypeToJava( + dataSchema.properties!.response as JSONSchema7, + false, + "ExampleEventData", + "response", + new Map(), + { + definitions, + standaloneTypes, + promotedUnionTypes: promoted, + }, + ); + + assert.equal(result.javaType, "TypedResponse"); + assert.equal(standaloneTypes.get("TypedResponse"), definitions.TypedResponse); +}); + test("nested discriminated array items use their named Java type", () => { const definitions: Record = { MessageResult: { @@ -478,6 +642,76 @@ function rpcSource(files: Map, name: string): string { return source; } +test("instructions reload reuses getSources result without generating a duplicate Java type", async () => { + const fixture = { + definitions: { + InstructionsGetSourcesResult: { + type: "object", + properties: { sources: { type: "array", items: { type: "string" } } }, + }, + OtherResult: { + type: "object", + properties: { sources: { type: "array", items: { type: "string" } } }, + }, + }, + session: { + instructions: { + getSources: { + rpcMethod: "session.instructions.getSources", + params: null, + result: { $ref: "#/definitions/InstructionsGetSourcesResult" }, + }, + reload: { + rpcMethod: "session.instructions.reload", + params: null, + result: { $ref: "#/definitions/InstructionsGetSourcesResult" }, + }, + }, + }, + }; + + const files = await renderRpcTypes(fixture, {}); + assert.match(rpcSource(files, "SessionInstructionsGetSourcesResult"), /List sources/); + assert.equal([...files.keys()].some((file) => file.endsWith("/SessionInstructionsReloadResult.java")), false); + + fixture.session.instructions.reload.result.$ref = "#/definitions/OtherResult"; + await assert.rejects(renderRpcTypes(fixture, {}), /Incompatible Java RPC result alias/); +}); + +test("customizations reload uses its shared schema result name in types and wrapper", async () => { + const fixture = { + definitions: { + CustomizationsReloadResult: { + type: "object", + properties: { warnings: { type: "array", items: { type: "string" } } }, + }, + OtherResult: { + type: "object", + properties: { warnings: { type: "array", items: { type: "string" } } }, + }, + }, + session: { + customizations: { + reload: { + rpcMethod: "session.customizations.reload", + params: null, + result: { $ref: "#/definitions/CustomizationsReloadResult" }, + }, + }, + }, + }; + + const types = await renderRpcTypes(fixture, {}); + assert.match(rpcSource(types, "CustomizationsReloadResult"), /List warnings/); + assert.equal([...types.keys()].some((file) => file.endsWith("/SessionCustomizationsReloadResult.java")), false); + const wrappers = await renderRpcWrappers(fixture); + assert.match(rpcSource(wrappers, "SessionCustomizationsApi"), /CompletableFuture reload\(/); + + fixture.session.customizations.reload.result.$ref = "#/definitions/OtherResult"; + await assert.rejects(renderRpcTypes(fixture, {}), /Incompatible Java RPC result name/); + await assert.rejects(renderRpcWrappers(fixture), /Incompatible Java RPC result name/); +}); + test("static OAuth config preserves the legacy four-argument constructor", () => { const typeName = "McpOauthRequiredStaticClientConfig"; const source = generateRpcClass(typeName, { @@ -543,6 +777,20 @@ test("nullable referenced RPC results retain their object DTO and typed wrapper" assert.match(wrapper, /caller\.invoke\("session\.accounts\.getCurrent", .*SessionAccount\.class\)/); assert.doesNotMatch(wrapper, /Void/); }); +test("session filesystem capabilities preserve the legacy one-argument constructor", () => { + const source = generateRpcClass("SessionFsSetProviderCapabilities", { + type: "object", + properties: { + sqlite: { type: "boolean" }, + binary: { type: "boolean" }, + }, + }, new Map(), "com.github.copilot.generated").code; + assert.match( + source, + /public SessionFsSetProviderCapabilities\(\s*Boolean sqlite\s*\) \{\s*this\(sqlite, null\);/s + ); +}); + for (const roots of [ ["FirstResult", "HistoricalResult"], ["FirstResult", "HistoricalResult", "LastResult"], @@ -728,6 +976,41 @@ test("x-legacy-parameters session requests keep sessionId injected by the wrappe ); }); +test("trusted human response ingress stays package-private", async () => { + const fixture = { + session: { + ui: { + handlePendingUserInput: { + rpcMethod: "session.ui.handlePendingUserInput", + visibility: "public", + params: { type: "object", properties: { sessionId: { type: "string" } } }, + result: { type: "object", properties: { success: { type: "boolean" } } }, + }, + handleHumanUserInput: { + rpcMethod: "session.ui.handleHumanUserInput", + visibility: "internal", + params: { type: "object", properties: { sessionId: { type: "string" } } }, + result: { type: "object", properties: { success: { type: "boolean" } } }, + }, + handleHumanExitPlanMode: { + rpcMethod: "session.ui.handleHumanExitPlanMode", + visibility: "internal", + params: { type: "object", properties: { sessionId: { type: "string" } } }, + result: { type: "object", properties: { success: { type: "boolean" } } }, + }, + }, + }, + definitions: {}, + } as Parameters[0]; + + const wrappers = await renderRpcWrappers(fixture); + const api = generatedFile(wrappers, "SessionUiApi")!; + assert.match(api, /public CompletableFuture handlePendingUserInput\(/); + assert.match(api, /\n CompletableFuture handleHumanUserInput\(/); + assert.match(api, /\n CompletableFuture handleHumanExitPlanMode\(/); + assert.doesNotMatch(api, /public CompletableFuture/); +}); + test("x-legacy-parameters rejects metadata that would not preserve the original API", async () => { await assert.rejects( renderRpcTypes(legacyRequestFixture(1, "server", ["contract", "scope"]), {}), diff --git a/java/scripts/codegen/java.ts b/java/scripts/codegen/java.ts index d4268f0a8f..076ea61848 100644 --- a/java/scripts/codegen/java.ts +++ b/java/scripts/codegen/java.ts @@ -24,6 +24,8 @@ const OUTPUT_ROOT = process.env.COPILOT_CODEGEN_OUTPUT_ROOT ?? REPO_ROOT; /** Event types to exclude from generation (internal/legacy types) */ const EXCLUDED_EVENT_TYPES = new Set(["session.import_legacy"]); +// Existing event unions are Object in the shipped Java API; opt in new unions without changing those signatures. +const PROMOTED_SESSION_EVENT_UNION_TYPES = new Set(["HumanResponseRecordedResponse"]); function isSchemaInternal(schema: JSONSchema7 | null | undefined): boolean { return typeof schema === "object" && @@ -37,8 +39,8 @@ function isSchemaExperimental(schema: JSONSchema7 | null | undefined): boolean { (schema as Record).stability === "experimental"; } -function appendExperimentalTypeApiNote(lines: string[], schema: JSONSchema7): void { - if (isSchemaExperimental(schema)) { +function appendExperimentalTypeApiNote(lines: string[], schema: JSONSchema7, experimental = isSchemaExperimental(schema)): void { + if (experimental) { lines.push(` *`); lines.push(` * @apiNote This type is experimental and may change in a future version.`); } @@ -514,6 +516,23 @@ function referencedDefinitionName(schema: JSONSchema7 | boolean | undefined): st return schema.$ref.slice("#/definitions/".length); } +export function collectDirectDiscriminatedUnionTypeNames( + root: JSONSchema7 | null, + definitions: Record, + promotedTypeNames: ReadonlySet, +): Set { + const typeNames = new Set(); + for (const property of Object.values(root?.properties ?? {})) { + const name = referencedDefinitionName(property); + if (!name || !promotedTypeNames.has(name)) continue; + const variants = resolveUnionVariants(definitions[name], definitions); + if (variants && variants.length > 1 && findDiscriminator(variants)) { + typeNames.add(name); + } + } + return typeNames; +} + function discriminatedUnionReferencesInVariants( typeName: string, definitions: Record @@ -1490,9 +1509,10 @@ interface EventVariant { description?: string; stability?: string; deprecated?: boolean; + dataDeprecated?: boolean; } -function extractEventVariants(schema: JSONSchema7): EventVariant[] { +export function extractEventVariants(schema: JSONSchema7): EventVariant[] { const definitions = schema.definitions as Record; const sessionEvent = definitions?.SessionEvent; if (!sessionEvent?.anyOf) throw new Error("Schema must have SessionEvent definition with anyOf"); @@ -1522,7 +1542,8 @@ function extractEventVariants(schema: JSONSchema7): EventVariant[] { dataSchema: dataSchema ?? null, description: resolved.description, stability: (variant as unknown as Record).stability as string | undefined, - deprecated: (variant as unknown as Record).deprecated === true, + deprecated: (resolved as unknown as Record).deprecated === true, + dataDeprecated: dataSchema !== undefined && "deprecated" in dataSchema && dataSchema.deprecated === true, }; }) .filter((v) => !EXCLUDED_EVENT_TYPES.has(v.typeName) && !isSchemaInternal(v.dataSchema)); @@ -1536,9 +1557,19 @@ async function generateSessionEvents(schemaPath: string): Promise { // Set module-level definitions for $ref resolution currentDefinitions = (schema.definitions ?? {}) as Record; pendingStandaloneTypes.clear(); + promotedNestedUnionTypes.clear(); promotedSealedUnionTypes.clear(); const variants = extractEventVariants(schema); + for (const variant of variants) { + for (const typeName of collectDirectDiscriminatedUnionTypeNames( + variant.dataSchema, + currentDefinitions, + PROMOTED_SESSION_EVENT_UNION_TYPES, + )) { + promotedNestedUnionTypes.add(typeName); + } + } const packageName = "com.github.copilot.generated"; const packageDir = `sdk/src/generated/java/com/github/copilot/generated`; @@ -1710,10 +1741,15 @@ function renderNestedType(nested: JavaClassDef, indentLevel: number, nestedTypes const lines: string[] = []; if (nested.kind === "enum") { + const allowUnknown = isOpenCustomizationReloadEnum(nested.name); lines.push(""); if (nested.description) { lines.push(`${ind}/** ${nested.description} */`); } + if (allowUnknown) { + lines.push(...renderOpenEnumClass(nested.name, nested.values || [], ind, true)); + return lines; + } lines.push(`${ind}public enum ${nested.name} {`); for (let i = 0; i < (nested.values || []).length; i++) { const v = nested.values![i]; @@ -1897,6 +1933,9 @@ export function renderEventVariantClass(variant: EventVariant, packageName: stri lines.push(hasUnionData ? ` /** Raw union payload for {@link ${variant.className}}, preserving every variant's fields. */` : ` /** Data payload for {@link ${variant.className}}. */`); + if (variant.dataDeprecated) { + lines.push(` @Deprecated`); + } lines.push(` @JsonIgnoreProperties(ignoreUnknown = true)`); lines.push(` @JsonInclude(JsonInclude.Include.NON_NULL)`); if (hasUnionData) { @@ -1997,6 +2036,50 @@ async function generatePendingStandaloneTypes( } } +function isOpenCustomizationReloadEnum(name: string): boolean { + return name === "CustomizationReloadStatus" || name === "CustomizationReloadSubsystem"; +} + +function renderOpenEnumClass( + name: string, + values: string[], + ind: string, + nested = false, + visibility: "public" | "internal" = "public" +): string[] { + const lines: string[] = []; + const visModifier = visibility === "public" ? "public " : ""; + lines.push(`${ind}${visModifier}${nested ? "static " : ""}final class ${name} {`); + for (const value of values) { + lines.push(`${ind} /** The {@code ${value}} variant. */`); + lines.push(`${ind} public static final ${name} ${toEnumConstant(value)} = new ${name}("${value}");`); + } + lines.push(`${ind} /** The default value when no recognized variant is available. */`); + lines.push(`${ind} public static final ${name} UNKNOWN = new ${name}("unknown");`); + lines.push(""); + lines.push(`${ind} private final String value;`); + lines.push(`${ind} private ${name}(String value) { this.value = value; }`); + lines.push(`${ind} @com.fasterxml.jackson.annotation.JsonValue`); + lines.push(`${ind} public String getValue() { return value; }`); + lines.push(`${ind} @com.fasterxml.jackson.annotation.JsonCreator`); + lines.push(`${ind} public static ${name} fromValue(String value) {`); + for (const value of values) { + lines.push(`${ind} if (${toEnumConstant(value)}.value.equals(value)) return ${toEnumConstant(value)};`); + } + lines.push(`${ind} if (UNKNOWN.value.equals(value)) return UNKNOWN;`); + lines.push(`${ind} if (value != null) return new ${name}(value);`); + lines.push(`${ind} throw new IllegalArgumentException("Unknown ${name} value: " + value);`); + lines.push(`${ind} }`); + lines.push(`${ind} @Override`); + lines.push(`${ind} public boolean equals(Object other) {`); + lines.push(`${ind} return other instanceof ${name} that && value.equals(that.value);`); + lines.push(`${ind} }`); + lines.push(`${ind} @Override`); + lines.push(`${ind} public int hashCode() { return value.hashCode(); }`); + lines.push(`${ind}}`); + return lines; +} + async function generateStandaloneEnum( name: string, schema: JSONSchema7, @@ -2006,7 +2089,9 @@ async function generateStandaloneEnum( visibility: "public" | "internal" = "public" ): Promise { const values = schema.enum as string[]; - const experimental = isSchemaExperimental(schema); + const extensibleFileEditKind = name === "ToolExecutionCompleteFileEditKind"; + const allowUnknown = isOpenCustomizationReloadEnum(name); + const experimental = isSchemaExperimental(schema) || allowUnknown; const lines: string[] = []; lines.push(COPYRIGHT); lines.push(""); @@ -2023,7 +2108,7 @@ async function generateStandaloneEnum( if (schema.description || experimental) { lines.push(`/**`); lines.push(` * ${schema.description ?? `Values for {@code ${name}}.`}`); - appendExperimentalTypeApiNote(lines, schema); + appendExperimentalTypeApiNote(lines, schema, experimental); lines.push(` *`); lines.push(` * @since 1.0.0`); lines.push(` */`); @@ -2033,26 +2118,65 @@ async function generateStandaloneEnum( } lines.push(GENERATED_ANNOTATION); const visModifier = visibility === "public" ? "public " : ""; - lines.push(`${visModifier}enum ${name} {`); - for (let i = 0; i < values.length; i++) { - const v = values[i]; - const comma = i < values.length - 1 ? "," : ";"; - lines.push(` /** The {@code ${v}} variant. */`); - lines.push(` ${toEnumConstant(v)}("${v}")${comma}`); - } - lines.push(""); - lines.push(` private final String value;`); - lines.push(` ${name}(String value) { this.value = value; }`); - lines.push(` @com.fasterxml.jackson.annotation.JsonValue`); - lines.push(` public String getValue() { return value; }`); - lines.push(` @com.fasterxml.jackson.annotation.JsonCreator`); - lines.push(` public static ${name} fromValue(String value) {`); - lines.push(` for (${name} v : values()) {`); - lines.push(` if (v.value.equals(value)) return v;`); - lines.push(` }`); - lines.push(` throw new IllegalArgumentException("Unknown ${name} value: " + value);`); - lines.push(` }`); - lines.push(`}`); + if (extensibleFileEditKind) { + lines.push(`${visModifier}final class ${name} {`); + for (const value of values) { + lines.push(` /** The {@code ${value}} variant. */`); + lines.push(` public static final ${name} ${toEnumConstant(value)} = new ${name}("${value}");`); + } + lines.push(` /** An explicit unknown file operation kind. */`); + lines.push(` public static final ${name} UNKNOWN = new ${name}("unknown");`); + lines.push(""); + lines.push(` private final String value;`); + lines.push(` private ${name}(String value) { this.value = value; }`); + lines.push(` @com.fasterxml.jackson.annotation.JsonValue`); + lines.push(` public String getValue() { return value; }`); + lines.push(` @com.fasterxml.jackson.annotation.JsonCreator`); + lines.push(` public static ${name} fromValue(String value) {`); + lines.push(` if (value == null) throw new IllegalArgumentException("Missing ${name} value");`); + for (const v of values) { + lines.push(` if ("${v}".equals(value)) return ${toEnumConstant(v)};`); + } + lines.push(` if ("unknown".equals(value)) return UNKNOWN;`); + lines.push(` return new ${name}(value);`); + lines.push(` }`); + lines.push(` @Override`); + lines.push(` public boolean equals(Object other) {`); + lines.push(` return other instanceof ${name} kind && value.equals(kind.value);`); + lines.push(` }`); + lines.push(` @Override`); + lines.push(` public int hashCode() { return value.hashCode(); }`); + lines.push(` @Override`); + lines.push(` public String toString() { return value; }`); + lines.push(`}`); + lines.push(""); + await writeGeneratedFile(`${packageDir}/${name}.java`, lines.join("\n")); + return; + } + if (allowUnknown) { + lines.push(...renderOpenEnumClass(name, values, "", false, visibility)); + } else { + lines.push(`${visModifier}enum ${name} {`); + for (let i = 0; i < values.length; i++) { + const v = values[i]; + const comma = i < values.length - 1 ? "," : ";"; + lines.push(` /** The {@code ${v}} variant. */`); + lines.push(` ${toEnumConstant(v)}("${v}")${comma}`); + } + lines.push(""); + lines.push(` private final String value;`); + lines.push(` ${name}(String value) { this.value = value; }`); + lines.push(` @com.fasterxml.jackson.annotation.JsonValue`); + lines.push(` public String getValue() { return value; }`); + lines.push(` @com.fasterxml.jackson.annotation.JsonCreator`); + lines.push(` public static ${name} fromValue(String value) {`); + lines.push(` for (${name} v : values()) {`); + lines.push(` if (v.value.equals(value)) return v;`); + lines.push(` }`); + lines.push(` throw new IllegalArgumentException("Unknown ${name} value: " + value);`); + lines.push(` }`); + lines.push(`}`); + } lines.push(""); await writeGeneratedFile(`${packageDir}/${name}.java`, lines.join("\n")); @@ -2068,7 +2192,7 @@ async function generateStandaloneRecord( ): Promise { const nestedTypes = new Map(); const { code, imports } = generateRpcClass(name, schema, nestedTypes, packageName, visibility); - const experimental = isSchemaExperimental(schema); + const experimental = isSchemaExperimental(schema) || name === "CustomizationReloadOutcome"; const lines: string[] = []; lines.push(COPYRIGHT); @@ -2098,7 +2222,7 @@ async function generateStandaloneRecord( if (schema.description || experimental) { lines.push(`/**`); lines.push(` * ${schema.description ?? `Data type {@code ${name}}.`}`); - appendExperimentalTypeApiNote(lines, schema); + appendExperimentalTypeApiNote(lines, schema, experimental); lines.push(` *`); lines.push(` * @since 1.0.0`); lines.push(` */`); @@ -2145,6 +2269,46 @@ function rpcMethodToClassName(rpcMethod: string): string { return rpcMethod.split(/[._-]/).map((p) => p.charAt(0).toUpperCase() + p.slice(1)).join(""); } +// Only alias deliberately equivalent results; globally collapsing shared refs changes existing Java API types. +const SHARED_RESULT_OWNERS = new Map([ + ["session.instructions.reload", "session.instructions.getSources"], +]); +const NAMED_RESULT_TYPES = new Map([ + ["session.customizations.reload", "CustomizationsReloadResult"], +]); + +function rpcResultClassName(rpcMethod: string): string { + return NAMED_RESULT_TYPES.get(rpcMethod) ?? `${rpcMethodToClassName(SHARED_RESULT_OWNERS.get(rpcMethod) ?? rpcMethod)}Result`; +} + +function validateSharedResultOwners(sections: Record[]): void { + const methods = new Map(sections.flatMap((section) => + collectRpcMethods(section).map(([, method]) => [method.rpcMethod, method] as const) + )); + for (const [alias, owner] of SHARED_RESULT_OWNERS) { + const aliasMethod = methods.get(alias); + if (!aliasMethod) continue; + const ref = aliasMethod.result?.$ref; + if ( + !ref?.startsWith("#/definitions/") || + ref !== methods.get(owner)?.result?.$ref || + !currentDefinitions[ref.slice("#/definitions/".length)]?.properties + ) { + throw new Error(`Incompatible Java RPC result alias: ${alias} must share an object result with ${owner}`); + } + } + for (const [methodName, typeName] of NAMED_RESULT_TYPES) { + const method = methods.get(methodName); + if (!method) continue; + if ( + method.result?.$ref !== `#/definitions/${typeName}` || + !currentDefinitions[typeName]?.properties + ) { + throw new Error(`Incompatible Java RPC result name: ${methodName} must reference the object ${typeName}`); + } + } +} + function schemaAllowsNull(schema: JSONSchema7): boolean { if (schema.type === "null" || (Array.isArray(schema.type) && schema.type.includes("null"))) { return true; @@ -2218,6 +2382,8 @@ export function generateRpcClass( const legacyFieldNames = className === "McpOauthRequiredStaticClientConfig" ? ["clientId", "clientSecret", "publicClient", "grantType"] + : className === "SessionFsSetProviderCapabilities" + ? ["sqlite"] : undefined; if (legacyFieldNames) { const legacyFields = legacyFieldNames.map((fieldName) => { @@ -2228,13 +2394,14 @@ export function generateRpcClass( return field; }); const omittedFields = fields.filter((field) => !legacyFieldNames.includes(field.javaName)); - if (omittedFields.length !== 1 || omittedFields[0].javaName !== "scope") { + const expectedAddedField = className === "SessionFsSetProviderCapabilities" ? "binary" : "scope"; + if (omittedFields.length !== 1 || omittedFields[0].javaName !== expectedAddedField) { throw new Error(`Unexpected compatibility fields for ${className}`); } lines.push(``); lines.push(` /**`); - lines.push(` * Creates a static OAuth client configuration without an explicit scope.`); + lines.push(` * Creates ${className === "SessionFsSetProviderCapabilities" ? "provider capabilities without binary reads" : "a static OAuth client configuration without an explicit scope"}.`); lines.push(` *`); for (const field of legacyFields) { const description = (field.description || field.propName) @@ -2528,6 +2695,7 @@ async function collectRpcTypes(schema: RpcSchema): Promise { promotedSealedUnionTypes.add(typeName); promotedNestedUnionTypes.add(typeName); } + validateSharedResultOwners(sections.map(([, section]) => section)); for (const [, sectionNode] of sections) { for (const [, method] of collectRpcMethods(sectionNode)) { @@ -2603,7 +2771,7 @@ async function collectRpcTypes(schema: RpcSchema): Promise { continue; } // Object with properties → generate a record class - const resultClassName = `${className}Result`; + const resultClassName = rpcResultClassName(method.rpcMethod); if (!generatedClasses.has(resultClassName)) { generatedClasses.set(resultClassName, true); allFiles.push(await generateRpcDataClass(resultClassName, resultSchema, packageName, packageDir, method.rpcMethod, "result", method.stability, method.deprecated === true, new Set(), method.visibility === "internal" ? "internal" : "public")); @@ -2863,7 +3031,7 @@ function wrapperResultClassName(method: RpcMethodNode): string { } // Preserve public method-specific names; internal methods share the named definition. if (resolved.type === "object" && resolved.properties && Object.keys(resolved.properties).length > 0) { - return method.visibility === "internal" ? refName : rpcMethodToClassName(method.rpcMethod) + "Result"; + return method.visibility === "internal" ? refName : rpcResultClassName(method.rpcMethod); } // Empty object (no properties) that is a named definition → use definition name if (resolved.type === "object" && !resolved.properties) { @@ -2886,7 +3054,7 @@ function wrapperResultClassName(method: RpcMethodNode): string { result.properties && Object.keys(result.properties).length > 0 ) { - return rpcMethodToClassName(method.rpcMethod) + "Result"; + return rpcResultClassName(method.rpcMethod); } if (result && typeof result === "object" && result.type === "array") { @@ -3497,6 +3665,11 @@ export async function renderRpcWrappers(schema: RpcSchema): Promise { // Set module-level definitions for $ref resolution in wrapper helpers currentDefinitions = (schema.definitions ?? {}) as Record; + validateSharedResultOwners([ + ...[schema.server, schema.session, schema.clientSession].filter( + (section): section is Record => section !== undefined + ), + ]); const packageName = "com.github.copilot.generated.rpc"; const packageDir = `sdk/src/generated/java/com/github/copilot/generated/rpc`; diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/AssistantMessageReasoningBlocks.java b/java/sdk/src/generated/java/com/github/copilot/generated/AssistantMessageReasoningBlocks.java index 3603e09f1e..e9b97f00f3 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/AssistantMessageReasoningBlocks.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/AssistantMessageReasoningBlocks.java @@ -29,6 +29,8 @@ public record AssistantMessageReasoningBlocks( /** Model provider that produced these reasoning blocks. */ @JsonProperty("provider") String provider, /** Provider-native reasoning items or content blocks preserved verbatim, in order. A single response can carry several, and provider signatures or identifiers may depend on their exact content and ordering. */ - @JsonProperty("blocks") List blocks + @JsonProperty("blocks") List blocks, + /** Anthropic Messages assistant block ordering preserved when the legacy reasoning-only representation cannot reproduce it exactly. Thinking and text blocks remain verbatim; tool-use entries retain identity and a payload fingerprint when later signed reasoning depends on them, and are hydrated from the message's tool requests during replay. */ + @JsonProperty("orderedBlocks") List orderedBlocks ) { } diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/HumanResponseActor.java b/java/sdk/src/generated/java/com/github/copilot/generated/HumanResponseActor.java new file mode 100644 index 0000000000..ef66119adf --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/HumanResponseActor.java @@ -0,0 +1,37 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: session-events.schema.json + +package com.github.copilot.generated; + +import javax.annotation.processing.Generated; + +/** + * Controlled provenance for a typed runtime response. Only `human_response`, minted by a trusted direct-interaction ingress, is human authorization evidence. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public enum HumanResponseActor { + /** The {@code human_response} variant. */ + HUMAN_RESPONSE("human_response"), + /** The {@code host_automation} variant. */ + HOST_AUTOMATION("host_automation"), + /** The {@code unknown} variant. */ + UNKNOWN("unknown"); + + private final String value; + HumanResponseActor(String value) { this.value = value; } + @com.fasterxml.jackson.annotation.JsonValue + public String getValue() { return value; } + @com.fasterxml.jackson.annotation.JsonCreator + public static HumanResponseActor fromValue(String value) { + for (HumanResponseActor v : values()) { + if (v.value.equals(value)) return v; + } + throw new IllegalArgumentException("Unknown HumanResponseActor value: " + value); + } +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/HumanResponseRecordedEvent.java b/java/sdk/src/generated/java/com/github/copilot/generated/HumanResponseRecordedEvent.java new file mode 100644 index 0000000000..f2a2aa9012 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/HumanResponseRecordedEvent.java @@ -0,0 +1,47 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: session-events.schema.json + +package com.github.copilot.generated; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Session event "human_response.recorded". Durable request-correlated evidence for a typed response to a runtime-owned question or plan review. + * @since 1.0.0 + */ +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonInclude(JsonInclude.Include.NON_NULL) +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public final class HumanResponseRecordedEvent extends SessionEvent { + + @Override + public String getType() { return "human_response.recorded"; } + + @JsonProperty("data") + private HumanResponseRecordedEventData data; + + public HumanResponseRecordedEventData getData() { return data; } + public void setData(HumanResponseRecordedEventData data) { this.data = data; } + + /** Data payload for {@link HumanResponseRecordedEvent}. */ + @JsonIgnoreProperties(ignoreUnknown = true) + @JsonInclude(JsonInclude.Include.NON_NULL) + public record HumanResponseRecordedEventData( + /** Request ID of the runtime-owned question or plan review. */ + @JsonProperty("requestId") String requestId, + /** Tool call ID that opened the request, when present. */ + @JsonProperty("toolCallId") String toolCallId, + /** Controlled actor provenance established at response ingress. */ + @JsonProperty("actor") HumanResponseActor actor, + /** Typed request and response payload. */ + @JsonProperty("response") HumanResponseRecordedResponse response + ) { + } +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/HumanResponseRecordedResponse.java b/java/sdk/src/generated/java/com/github/copilot/generated/HumanResponseRecordedResponse.java new file mode 100644 index 0000000000..65efb2e2e0 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/HumanResponseRecordedResponse.java @@ -0,0 +1,36 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.generated; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonSubTypes; +import com.fasterxml.jackson.annotation.JsonTypeInfo; +import javax.annotation.processing.Generated; + +/** + * Exact runtime-owned question or reviewed plan paired with the typed response that settled it. + * + * @since 1.0.0 + */ +@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.EXISTING_PROPERTY, property = "responseKind", visible = true) +@JsonSubTypes({ + @JsonSubTypes.Type(value = HumanResponseRecordedResponseAskUser.class, name = "ask_user"), + @JsonSubTypes.Type(value = HumanResponseRecordedResponseUserInput.class, name = "user_input"), + @JsonSubTypes.Type(value = HumanResponseRecordedResponseExitPlanMode.class, name = "exit_plan_mode") +}) +@JsonIgnoreProperties(ignoreUnknown = true) +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public abstract class HumanResponseRecordedResponse { + + /** + * Returns the discriminator value for this variant. + * + * @return the responseKind discriminator + */ + public abstract String getResponseKind(); +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/HumanResponseRecordedResponseAskUser.java b/java/sdk/src/generated/java/com/github/copilot/generated/HumanResponseRecordedResponseAskUser.java new file mode 100644 index 0000000000..03afda0422 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/HumanResponseRecordedResponseAskUser.java @@ -0,0 +1,52 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.generated; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import java.util.Map; +import javax.annotation.processing.Generated; + +/** + * Variant {@code ask_user} of {@link HumanResponseRecordedResponse}. + * + * @since 1.0.0 + */ +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonInclude(JsonInclude.Include.NON_NULL) +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public final class HumanResponseRecordedResponseAskUser extends HumanResponseRecordedResponse { + + @JsonProperty("responseKind") + private final String responseKind = "ask_user"; + + @Override + public String getResponseKind() { return responseKind; } + + /** Exact answer content accepted from the user. */ + @JsonProperty("content") + private Map content; + + /** Exact question displayed to the user. */ + @JsonProperty("message") + private String message; + + /** Exact response schema displayed to the user. */ + @JsonProperty("requestedSchema") + private ElicitationRequestedSchema requestedSchema; + + public Map getContent() { return content; } + public void setContent(Map content) { this.content = content; } + + public String getMessage() { return message; } + public void setMessage(String message) { this.message = message; } + + public ElicitationRequestedSchema getRequestedSchema() { return requestedSchema; } + public void setRequestedSchema(ElicitationRequestedSchema requestedSchema) { this.requestedSchema = requestedSchema; } +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/HumanResponseRecordedResponseExitPlanMode.java b/java/sdk/src/generated/java/com/github/copilot/generated/HumanResponseRecordedResponseExitPlanMode.java new file mode 100644 index 0000000000..0d13b119da --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/HumanResponseRecordedResponseExitPlanMode.java @@ -0,0 +1,87 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.generated; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import java.util.List; +import javax.annotation.processing.Generated; + +/** + * Variant {@code exit_plan_mode} of {@link HumanResponseRecordedResponse}. + * + * @since 1.0.0 + */ +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonInclude(JsonInclude.Include.NON_NULL) +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public final class HumanResponseRecordedResponseExitPlanMode extends HumanResponseRecordedResponse { + + @JsonProperty("responseKind") + private final String responseKind = "exit_plan_mode"; + + @Override + public String getResponseKind() { return responseKind; } + + /** Exact plan summary displayed to the user. */ + @JsonProperty("summary") + private String summary; + + /** Exact full plan content available from the review UI. */ + @JsonProperty("planContent") + private String planContent; + + /** Actions offered by the plan review UI. */ + @JsonProperty("actions") + private List actions; + + /** Action the plan review UI recommended. */ + @JsonProperty("recommendedAction") + private ExitPlanModeAction recommendedAction; + + /** Whether the user approved the reviewed plan. */ + @JsonProperty("approved") + private Boolean approved; + + /** Action selected by the user, when applicable. */ + @JsonProperty("selectedAction") + private ExitPlanModeAction selectedAction; + + /** Whether the selected response requested edit auto-approval. */ + @JsonProperty("autoApproveEdits") + private Boolean autoApproveEdits; + + /** Exact feedback submitted with the plan decision, when present. */ + @JsonProperty("feedback") + private String feedback; + + public String getSummary() { return summary; } + public void setSummary(String summary) { this.summary = summary; } + + public String getPlanContent() { return planContent; } + public void setPlanContent(String planContent) { this.planContent = planContent; } + + public List getActions() { return actions; } + public void setActions(List actions) { this.actions = actions; } + + public ExitPlanModeAction getRecommendedAction() { return recommendedAction; } + public void setRecommendedAction(ExitPlanModeAction recommendedAction) { this.recommendedAction = recommendedAction; } + + public Boolean getApproved() { return approved; } + public void setApproved(Boolean approved) { this.approved = approved; } + + public ExitPlanModeAction getSelectedAction() { return selectedAction; } + public void setSelectedAction(ExitPlanModeAction selectedAction) { this.selectedAction = selectedAction; } + + public Boolean getAutoApproveEdits() { return autoApproveEdits; } + public void setAutoApproveEdits(Boolean autoApproveEdits) { this.autoApproveEdits = autoApproveEdits; } + + public String getFeedback() { return feedback; } + public void setFeedback(String feedback) { this.feedback = feedback; } +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/HumanResponseRecordedResponseUserInput.java b/java/sdk/src/generated/java/com/github/copilot/generated/HumanResponseRecordedResponseUserInput.java new file mode 100644 index 0000000000..2845d6169a --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/HumanResponseRecordedResponseUserInput.java @@ -0,0 +1,66 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.generated; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import java.util.List; +import javax.annotation.processing.Generated; + +/** + * Variant {@code user_input} of {@link HumanResponseRecordedResponse}. + * + * @since 1.0.0 + */ +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonInclude(JsonInclude.Include.NON_NULL) +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public final class HumanResponseRecordedResponseUserInput extends HumanResponseRecordedResponse { + + @JsonProperty("responseKind") + private final String responseKind = "user_input"; + + @Override + public String getResponseKind() { return responseKind; } + + /** Exact question displayed to the user. */ + @JsonProperty("question") + private String question; + + /** Exact choices displayed to the user, when the request offered choices. */ + @JsonProperty("choices") + private List choices; + + /** Whether the displayed request allowed a free-form answer. */ + @JsonProperty("allowFreeform") + private Boolean allowFreeform; + + /** Exact selected or free-form answer submitted by the user. */ + @JsonProperty("answer") + private String answer; + + /** Whether the answer was typed as free-form text rather than selected from the displayed choices. */ + @JsonProperty("wasFreeform") + private Boolean wasFreeform; + + public String getQuestion() { return question; } + public void setQuestion(String question) { this.question = question; } + + public List getChoices() { return choices; } + public void setChoices(List choices) { this.choices = choices; } + + public Boolean getAllowFreeform() { return allowFreeform; } + public void setAllowFreeform(Boolean allowFreeform) { this.allowFreeform = allowFreeform; } + + public String getAnswer() { return answer; } + public void setAnswer(String answer) { this.answer = answer; } + + public Boolean getWasFreeform() { return wasFreeform; } + public void setWasFreeform(Boolean wasFreeform) { this.wasFreeform = wasFreeform; } +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/SessionEvent.java b/java/sdk/src/generated/java/com/github/copilot/generated/SessionEvent.java index 33abf0411a..726dd21975 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/SessionEvent.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/SessionEvent.java @@ -96,6 +96,7 @@ @JsonSubTypes.Type(value = ToolUserRequestedEvent.class, name = "tool.user_requested"), @JsonSubTypes.Type(value = ToolExecutionStartEvent.class, name = "tool.execution_start"), @JsonSubTypes.Type(value = ToolExecutionPartialResultEvent.class, name = "tool.execution_partial_result"), + @JsonSubTypes.Type(value = ToolShellOutputEvent.class, name = "tool.shell_output"), @JsonSubTypes.Type(value = ToolExecutionProgressEvent.class, name = "tool.execution_progress"), @JsonSubTypes.Type(value = ToolExecutionCompleteEvent.class, name = "tool.execution_complete"), @JsonSubTypes.Type(value = ToolSearchActivatedEvent.class, name = "tool_search.activated"), @@ -152,6 +153,7 @@ @JsonSubTypes.Type(value = CapabilitiesChangedEvent.class, name = "capabilities.changed"), @JsonSubTypes.Type(value = ExitPlanModeRequestedEvent.class, name = "exit_plan_mode.requested"), @JsonSubTypes.Type(value = ExitPlanModeCompletedEvent.class, name = "exit_plan_mode.completed"), + @JsonSubTypes.Type(value = HumanResponseRecordedEvent.class, name = "human_response.recorded"), @JsonSubTypes.Type(value = SessionToolsUpdatedEvent.class, name = "session.tools_updated"), @JsonSubTypes.Type(value = SessionBackgroundTasksChangedEvent.class, name = "session.background_tasks_changed"), @JsonSubTypes.Type(value = WorkflowRunUpdatedEvent.class, name = "workflow.run_updated"), @@ -249,6 +251,7 @@ public abstract sealed class SessionEvent permits ToolUserRequestedEvent, ToolExecutionStartEvent, ToolExecutionPartialResultEvent, + ToolShellOutputEvent, ToolExecutionProgressEvent, ToolExecutionCompleteEvent, ToolSearchActivatedEvent, @@ -305,6 +308,7 @@ public abstract sealed class SessionEvent permits CapabilitiesChangedEvent, ExitPlanModeRequestedEvent, ExitPlanModeCompletedEvent, + HumanResponseRecordedEvent, SessionToolsUpdatedEvent, SessionBackgroundTasksChangedEvent, WorkflowRunUpdatedEvent, diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/SessionModelChangeEvent.java b/java/sdk/src/generated/java/com/github/copilot/generated/SessionModelChangeEvent.java index 4ea1dbd46d..09e39c584d 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/SessionModelChangeEvent.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/SessionModelChangeEvent.java @@ -42,6 +42,8 @@ public record SessionModelChangeEventData( @JsonProperty("previousReasoningEffort") String previousReasoningEffort, /** Reasoning effort level after the model change, if applicable */ @JsonProperty("reasoningEffort") String reasoningEffort, + /** Model that owns effort embedded in an authored model selection. Omitted for independent reasoning-effort overrides and legacy events. */ + @JsonProperty("reasoningEffortModel") String reasoningEffortModel, /** Reasoning summary mode before the model change, if applicable */ @JsonProperty("previousReasoningSummary") ReasoningSummary previousReasoningSummary, /** Reasoning summary mode after the model change, if applicable */ diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/SessionResumeEvent.java b/java/sdk/src/generated/java/com/github/copilot/generated/SessionResumeEvent.java index de54f8a3ce..4120d9e7c8 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/SessionResumeEvent.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/SessionResumeEvent.java @@ -45,6 +45,8 @@ public record SessionResumeEventData( @JsonProperty("selectedModel") String selectedModel, /** Reasoning effort level used for model calls, if applicable (e.g. "none", "low", "medium", "high", "xhigh", "max") */ @JsonProperty("reasoningEffort") String reasoningEffort, + /** Model that owns effort embedded in an authored model selection. Omitted for independent reasoning-effort overrides and legacy events. */ + @JsonProperty("reasoningEffortModel") String reasoningEffortModel, /** Reasoning summary mode used for model calls, if applicable (e.g. "none", "concise", "detailed") */ @JsonProperty("reasoningSummary") ReasoningSummary reasoningSummary, /** Output verbosity level used for model calls, if applicable (e.g. "low", "medium", "high") */ diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/SessionStartEvent.java b/java/sdk/src/generated/java/com/github/copilot/generated/SessionStartEvent.java index b977ae036f..db7336ce68 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/SessionStartEvent.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/SessionStartEvent.java @@ -49,6 +49,8 @@ public record SessionStartEventData( @JsonProperty("selectedModel") String selectedModel, /** Reasoning effort level used for model calls, if applicable (e.g. "none", "low", "medium", "high", "xhigh", "max") */ @JsonProperty("reasoningEffort") String reasoningEffort, + /** Model that owns effort embedded in an authored model selection. Omitted for independent reasoning-effort overrides and legacy events. */ + @JsonProperty("reasoningEffortModel") String reasoningEffortModel, /** Reasoning summary mode used for model calls, if applicable (e.g. "none", "concise", "detailed") */ @JsonProperty("reasoningSummary") ReasoningSummary reasoningSummary, /** Output verbosity level used for model calls, if applicable (e.g. "low", "medium", "high") */ diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/ToolExecutionCompleteEvent.java b/java/sdk/src/generated/java/com/github/copilot/generated/ToolExecutionCompleteEvent.java index 440dc6eaaf..1f2d180587 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/ToolExecutionCompleteEvent.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/ToolExecutionCompleteEvent.java @@ -11,6 +11,7 @@ import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; import com.github.copilot.CopilotExperimental; +import java.util.List; import java.util.Map; import javax.annotation.processing.Generated; @@ -66,6 +67,9 @@ public record ToolExecutionCompleteEventData( /** Experimental shell completion facts captured before the persisted result contents are stripped. */ @CopilotExperimental @JsonProperty("shellExecution") ToolExecutionCompleteShellExecution shellExecution, + /** Experimental. File mutations actually committed by a built-in file editing tool, in execution order. Present on successful edits and on partial failures when earlier mutations were committed. Paths are absolute in the session filesystem namespace. */ + @CopilotExperimental + @JsonProperty("fileEdits") List fileEdits, /** Tool call ID of the parent tool invocation when this event originates from a sub-agent */ @JsonProperty("parentToolCallId") String parentToolCallId, /** Experimental HydraFusion attribution for this tool completion. */ diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/ToolExecutionCompleteFileEdit.java b/java/sdk/src/generated/java/com/github/copilot/generated/ToolExecutionCompleteFileEdit.java new file mode 100644 index 0000000000..02a6db87d3 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/ToolExecutionCompleteFileEdit.java @@ -0,0 +1,33 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: session-events.schema.json + +package com.github.copilot.generated; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.github.copilot.CopilotExperimental; +import javax.annotation.processing.Generated; + +/** + * A file mutation that was actually committed by a built-in file editing tool. + * + * @apiNote This type is experimental and may change in a future version. + * + * @since 1.0.0 + */ +@CopilotExperimental +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record ToolExecutionCompleteFileEdit( + /** Absolute path in the session filesystem namespace. */ + @JsonProperty("path") String path, + /** Kind of mutation committed at this path. */ + @JsonProperty("kind") ToolExecutionCompleteFileEditKind kind +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/ToolExecutionCompleteFileEditKind.java b/java/sdk/src/generated/java/com/github/copilot/generated/ToolExecutionCompleteFileEditKind.java new file mode 100644 index 0000000000..cdf2f3aa50 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/ToolExecutionCompleteFileEditKind.java @@ -0,0 +1,53 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: session-events.schema.json + +package com.github.copilot.generated; + +import com.github.copilot.CopilotExperimental; +import javax.annotation.processing.Generated; + +/** + * Kind of file mutation committed by a built-in editing tool. + * + * @apiNote This type is experimental and may change in a future version. + * + * @since 1.0.0 + */ +@CopilotExperimental +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public final class ToolExecutionCompleteFileEditKind { + /** The {@code create} variant. */ + public static final ToolExecutionCompleteFileEditKind CREATE = new ToolExecutionCompleteFileEditKind("create"); + /** The {@code edit} variant. */ + public static final ToolExecutionCompleteFileEditKind EDIT = new ToolExecutionCompleteFileEditKind("edit"); + /** The {@code delete} variant. */ + public static final ToolExecutionCompleteFileEditKind DELETE = new ToolExecutionCompleteFileEditKind("delete"); + /** An explicit unknown file operation kind. */ + public static final ToolExecutionCompleteFileEditKind UNKNOWN = new ToolExecutionCompleteFileEditKind("unknown"); + + private final String value; + private ToolExecutionCompleteFileEditKind(String value) { this.value = value; } + @com.fasterxml.jackson.annotation.JsonValue + public String getValue() { return value; } + @com.fasterxml.jackson.annotation.JsonCreator + public static ToolExecutionCompleteFileEditKind fromValue(String value) { + if (value == null) throw new IllegalArgumentException("Missing ToolExecutionCompleteFileEditKind value"); + if ("create".equals(value)) return CREATE; + if ("edit".equals(value)) return EDIT; + if ("delete".equals(value)) return DELETE; + if ("unknown".equals(value)) return UNKNOWN; + return new ToolExecutionCompleteFileEditKind(value); + } + @Override + public boolean equals(Object other) { + return other instanceof ToolExecutionCompleteFileEditKind kind && value.equals(kind.value); + } + @Override + public int hashCode() { return value.hashCode(); } + @Override + public String toString() { return value; } +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/ToolExecutionPartialResultEvent.java b/java/sdk/src/generated/java/com/github/copilot/generated/ToolExecutionPartialResultEvent.java index e78d4d2a78..d8068f687a 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/ToolExecutionPartialResultEvent.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/ToolExecutionPartialResultEvent.java @@ -13,9 +13,10 @@ import javax.annotation.processing.Generated; /** - * Session event "tool.execution_partial_result". Streaming tool execution output for incremental result display + * Session event "tool.execution_partial_result". Deprecated merged replacement snapshot of shell output. Use tool.shell_output for append-only, stream-tagged output instead. * @since 1.0.0 */ +@Deprecated @JsonIgnoreProperties(ignoreUnknown = true) @JsonInclude(JsonInclude.Include.NON_NULL) @javax.annotation.processing.Generated("copilot-sdk-codegen") @@ -31,12 +32,13 @@ public final class ToolExecutionPartialResultEvent extends SessionEvent { public void setData(ToolExecutionPartialResultEventData data) { this.data = data; } /** Data payload for {@link ToolExecutionPartialResultEvent}. */ + @Deprecated @JsonIgnoreProperties(ignoreUnknown = true) @JsonInclude(JsonInclude.Include.NON_NULL) public record ToolExecutionPartialResultEventData( /** Tool call ID this partial result belongs to */ @JsonProperty("toolCallId") String toolCallId, - /** Incremental output chunk from the running tool */ + /** Merged replacement snapshot from the running shell, not an append-only chunk */ @JsonProperty("partialOutput") String partialOutput ) { } diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/ToolShellOutputEvent.java b/java/sdk/src/generated/java/com/github/copilot/generated/ToolShellOutputEvent.java new file mode 100644 index 0000000000..39e496ddc7 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/ToolShellOutputEvent.java @@ -0,0 +1,47 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: session-events.schema.json + +package com.github.copilot.generated; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Session event "tool.shell_output". Live, append-only shell output. Not persisted or replayed to late subscribers. Text is decoded and redacted per chunk; chunks need not contain complete lines. + * @since 1.0.0 + */ +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonInclude(JsonInclude.Include.NON_NULL) +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public final class ToolShellOutputEvent extends SessionEvent { + + @Override + public String getType() { return "tool.shell_output"; } + + @JsonProperty("data") + private ToolShellOutputEventData data; + + public ToolShellOutputEventData getData() { return data; } + public void setData(ToolShellOutputEventData data) { this.data = data; } + + /** Data payload for {@link ToolShellOutputEvent}. */ + @JsonIgnoreProperties(ignoreUnknown = true) + @JsonInclude(JsonInclude.Include.NON_NULL) + public record ToolShellOutputEventData( + /** Tool call ID that owns this shell output */ + @JsonProperty("toolCallId") String toolCallId, + /** Output source. Omission means stdout. Terminal output has no separate stdout/stderr attribution. */ + @JsonProperty("stream") ToolShellOutputStream stream, + /** New output to append, without synthetic shell-result markers or stream-switch separators */ + @JsonProperty("text") String text, + /** Zero-based publication sequence across all output streams for this tool call. Not a byte offset or an OS write-order guarantee. */ + @JsonProperty("sequence") Long sequence + ) { + } +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/ToolShellOutputStream.java b/java/sdk/src/generated/java/com/github/copilot/generated/ToolShellOutputStream.java new file mode 100644 index 0000000000..6a424b0772 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/ToolShellOutputStream.java @@ -0,0 +1,37 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: session-events.schema.json + +package com.github.copilot.generated; + +import javax.annotation.processing.Generated; + +/** + * Shell output source. Terminal output has no separate stdout/stderr attribution. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public enum ToolShellOutputStream { + /** The {@code stdout} variant. */ + STDOUT("stdout"), + /** The {@code stderr} variant. */ + STDERR("stderr"), + /** The {@code terminal} variant. */ + TERMINAL("terminal"); + + private final String value; + ToolShellOutputStream(String value) { this.value = value; } + @com.fasterxml.jackson.annotation.JsonValue + public String getValue() { return value; } + @com.fasterxml.jackson.annotation.JsonCreator + public static ToolShellOutputStream fromValue(String value) { + for (ToolShellOutputStream v : values()) { + if (v.value.equals(value)) return v; + } + throw new IllegalArgumentException("Unknown ToolShellOutputStream value: " + value); + } +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/AccountsAcquireEntraTokenParams.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/AccountsAcquireEntraTokenParams.java deleted file mode 100644 index 6ab4bd5c19..0000000000 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/AccountsAcquireEntraTokenParams.java +++ /dev/null @@ -1,41 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - *--------------------------------------------------------------------------------------------*/ - -// AUTO-GENERATED FILE - DO NOT EDIT -// Generated from: api.schema.json - -package com.github.copilot.generated.rpc; - -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.annotation.JsonProperty; -import com.github.copilot.CopilotExperimental; -import java.util.List; -import javax.annotation.processing.Generated; - -/** - * OneAuth token request supplied by a trusted host application. - * - * @apiNote This method is experimental and may change in a future version. - * @since 1.0.0 - */ -@CopilotExperimental -@javax.annotation.processing.Generated("copilot-sdk-codegen") -@JsonInclude(JsonInclude.Include.NON_NULL) -@JsonIgnoreProperties(ignoreUnknown = true) -record AccountsAcquireEntraTokenParams( - /** Public client application id. */ - @JsonProperty("clientId") String clientId, - /** Tenant id or tenant selector, such as common or organizations. */ - @JsonProperty("tenantId") String tenantId, - /** Broker redirect URI registered for the client. Required: the OneAuth broker validates a non-empty, registered redirect URI for the public client (MSAL broker registration), so this is not a browser-flow vestige and cannot be omitted. */ - @JsonProperty("redirectUri") String redirectUri, - /** Exact delegated scopes to request. */ - @JsonProperty("scopes") List scopes, - /** Whether the broker may show interaction. */ - @JsonProperty("interaction") EntraTokenInteraction interaction, - /** Previously rejected token that OneAuth must bypass during renewal. */ - @JsonProperty("accessTokenToRenew") String accessTokenToRenew -) { -} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/CurrentModel.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/CurrentModel.java index bf5f9a8b97..3088672b69 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/CurrentModel.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/CurrentModel.java @@ -23,6 +23,8 @@ public record CurrentModel( /** Currently active model identifier */ @JsonProperty("modelId") String modelId, + /** Captured base model to restore when leaving plan mode. Omitted outside plan mode or when no plan override has captured a base model. Persistent agent model requirements apply to this model rather than the temporary plan model. */ + @JsonProperty("planBaseModelId") String planBaseModelId, /** Reasoning effort level currently applied to the active model, when one is set. Reads `Session.getReasoningEffort()` synchronously after `getSelectedModel()` resolves so the two values are reported as a snapshot. */ @JsonProperty("reasoningEffort") String reasoningEffort, /** Context tier for models that support multiple context-window sizes. */ diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/EntraTokenAcquireResult.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/CustomizationReloadOutcome.java similarity index 54% rename from java/sdk/src/generated/java/com/github/copilot/generated/rpc/EntraTokenAcquireResult.java rename to java/sdk/src/generated/java/com/github/copilot/generated/rpc/CustomizationReloadOutcome.java index afc0016c98..d8941b4567 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/EntraTokenAcquireResult.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/CustomizationReloadOutcome.java @@ -8,32 +8,28 @@ package com.github.copilot.generated.rpc; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonSubTypes; -import com.fasterxml.jackson.annotation.JsonTypeInfo; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; import com.github.copilot.CopilotExperimental; import javax.annotation.processing.Generated; /** - * Result of a OneAuth token acquisition. + * Result of one customization reload component. * * @apiNote This type is experimental and may change in a future version. * * @since 1.0.0 */ -@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "status", visible = true) -@JsonSubTypes({ - @JsonSubTypes.Type(value = EntraTokenAcquireResultOk.class, name = "ok"), - @JsonSubTypes.Type(value = EntraTokenAcquireResultInteractionRequired.class, name = "interaction-required") -}) @CopilotExperimental -@JsonIgnoreProperties(ignoreUnknown = true) @javax.annotation.processing.Generated("copilot-sdk-codegen") -abstract class EntraTokenAcquireResult { - - /** - * Returns the discriminator value for this variant. - * - * @return the status discriminator - */ - public abstract String getStatus(); +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record CustomizationReloadOutcome( + /** Component whose reload was attempted or skipped */ + @JsonProperty("subsystem") CustomizationReloadSubsystem subsystem, + /** Whether the component reloaded, was skipped, or failed */ + @JsonProperty("status") CustomizationReloadStatus status, + /** Reason for a skipped component or description of a failure, when available */ + @JsonProperty("detail") String detail +) { } diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/CustomizationReloadStatus.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/CustomizationReloadStatus.java new file mode 100644 index 0000000000..b34528549b --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/CustomizationReloadStatus.java @@ -0,0 +1,51 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.generated.rpc; + +import com.github.copilot.CopilotExperimental; +import javax.annotation.processing.Generated; + +/** + * Result of reloading a customization component. + * + * @apiNote This type is experimental and may change in a future version. + * + * @since 1.0.0 + */ +@CopilotExperimental +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public final class CustomizationReloadStatus { + /** The {@code reloaded} variant. */ + public static final CustomizationReloadStatus RELOADED = new CustomizationReloadStatus("reloaded"); + /** The {@code skipped} variant. */ + public static final CustomizationReloadStatus SKIPPED = new CustomizationReloadStatus("skipped"); + /** The {@code failed} variant. */ + public static final CustomizationReloadStatus FAILED = new CustomizationReloadStatus("failed"); + /** The default value when no recognized variant is available. */ + public static final CustomizationReloadStatus UNKNOWN = new CustomizationReloadStatus("unknown"); + + private final String value; + private CustomizationReloadStatus(String value) { this.value = value; } + @com.fasterxml.jackson.annotation.JsonValue + public String getValue() { return value; } + @com.fasterxml.jackson.annotation.JsonCreator + public static CustomizationReloadStatus fromValue(String value) { + if (RELOADED.value.equals(value)) return RELOADED; + if (SKIPPED.value.equals(value)) return SKIPPED; + if (FAILED.value.equals(value)) return FAILED; + if (UNKNOWN.value.equals(value)) return UNKNOWN; + if (value != null) return new CustomizationReloadStatus(value); + throw new IllegalArgumentException("Unknown CustomizationReloadStatus value: " + value); + } + @Override + public boolean equals(Object other) { + return other instanceof CustomizationReloadStatus that && value.equals(that.value); + } + @Override + public int hashCode() { return value.hashCode(); } +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/CustomizationReloadSubsystem.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/CustomizationReloadSubsystem.java new file mode 100644 index 0000000000..989bde0a7c --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/CustomizationReloadSubsystem.java @@ -0,0 +1,66 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.generated.rpc; + +import com.github.copilot.CopilotExperimental; +import javax.annotation.processing.Generated; + +/** + * Component of session customization discovery. + * + * @apiNote This type is experimental and may change in a future version. + * + * @since 1.0.0 + */ +@CopilotExperimental +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public final class CustomizationReloadSubsystem { + /** The {@code repositoryContext} variant. */ + public static final CustomizationReloadSubsystem REPOSITORYCONTEXT = new CustomizationReloadSubsystem("repositoryContext"); + /** The {@code instructions} variant. */ + public static final CustomizationReloadSubsystem INSTRUCTIONS = new CustomizationReloadSubsystem("instructions"); + /** The {@code plugins} variant. */ + public static final CustomizationReloadSubsystem PLUGINS = new CustomizationReloadSubsystem("plugins"); + /** The {@code hooks} variant. */ + public static final CustomizationReloadSubsystem HOOKS = new CustomizationReloadSubsystem("hooks"); + /** The {@code skills} variant. */ + public static final CustomizationReloadSubsystem SKILLS = new CustomizationReloadSubsystem("skills"); + /** The {@code agents} variant. */ + public static final CustomizationReloadSubsystem AGENTS = new CustomizationReloadSubsystem("agents"); + /** The {@code mcp} variant. */ + public static final CustomizationReloadSubsystem MCP = new CustomizationReloadSubsystem("mcp"); + /** The {@code extensions} variant. */ + public static final CustomizationReloadSubsystem EXTENSIONS = new CustomizationReloadSubsystem("extensions"); + /** The default value when no recognized variant is available. */ + public static final CustomizationReloadSubsystem UNKNOWN = new CustomizationReloadSubsystem("unknown"); + + private final String value; + private CustomizationReloadSubsystem(String value) { this.value = value; } + @com.fasterxml.jackson.annotation.JsonValue + public String getValue() { return value; } + @com.fasterxml.jackson.annotation.JsonCreator + public static CustomizationReloadSubsystem fromValue(String value) { + if (REPOSITORYCONTEXT.value.equals(value)) return REPOSITORYCONTEXT; + if (INSTRUCTIONS.value.equals(value)) return INSTRUCTIONS; + if (PLUGINS.value.equals(value)) return PLUGINS; + if (HOOKS.value.equals(value)) return HOOKS; + if (SKILLS.value.equals(value)) return SKILLS; + if (AGENTS.value.equals(value)) return AGENTS; + if (MCP.value.equals(value)) return MCP; + if (EXTENSIONS.value.equals(value)) return EXTENSIONS; + if (UNKNOWN.value.equals(value)) return UNKNOWN; + if (value != null) return new CustomizationReloadSubsystem(value); + throw new IllegalArgumentException("Unknown CustomizationReloadSubsystem value: " + value); + } + @Override + public boolean equals(Object other) { + return other instanceof CustomizationReloadSubsystem that && value.equals(that.value); + } + @Override + public int hashCode() { return value.hashCode(); } +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/CustomizationsReloadResult.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/CustomizationsReloadResult.java new file mode 100644 index 0000000000..2f84a8ee4b --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/CustomizationsReloadResult.java @@ -0,0 +1,35 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.github.copilot.CopilotExperimental; +import java.util.List; +import javax.annotation.processing.Generated; + +/** + * Results of reloading discovered session customizations. Inspect outcomes for reloaded, skipped, or failed subsystems; a rejection may follow partial mutation. Changes to the model-facing prompt and tools apply on the next turn. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ +@CopilotExperimental +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record CustomizationsReloadResult( + /** Warnings from skill discovery */ + @JsonProperty("warnings") List warnings, + /** Errors from any component that could not be refreshed */ + @JsonProperty("errors") List errors, + /** Outcome of each component in reload order; a skipped component was not configured or loaded */ + @JsonProperty("outcomes") List outcomes +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/DiscoveredModel.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/DiscoveredModel.java new file mode 100644 index 0000000000..c019f85165 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/DiscoveredModel.java @@ -0,0 +1,49 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.github.copilot.CopilotExperimental; +import java.time.OffsetDateTime; +import java.util.List; +import javax.annotation.processing.Generated; + +/** + * A model offered for agent conversations. Missing capability metadata does not disqualify a candidate. Models known to be incompatible, such as embedding-only models, are excluded by the adapter. + * + * @apiNote This type is experimental and may change in a future version. + * + * @since 1.0.0 + */ +@CopilotExperimental +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record DiscoveredModel( + /** Provider-native model identifier. */ + @JsonProperty("id") String id, + /** Provider-reported display name. */ + @JsonProperty("name") String name, + /** Attribution for the adapter that produced this model row. */ + @JsonProperty("provenance") ModelProviderProvenance provenance, + /** Provider-reported artifact digest. */ + @JsonProperty("digest") String digest, + /** Provider-reported last-modified timestamp. */ + @JsonProperty("modifiedAt") OffsetDateTime modifiedAt, + /** Provider-reported artifact size in bytes. */ + @JsonProperty("sizeBytes") Long sizeBytes, + /** Provider-reported model artifact details. */ + @JsonProperty("details") ModelArtifactDetails details, + /** Provider-reported model capabilities. Omitted capability fields are unknown; explicit false values are preserved. */ + @JsonProperty("capabilities") ModelCapabilities capabilities, + /** Non-fatal warnings encountered while enriching this model. */ + @JsonProperty("warnings") List warnings +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/EntraTokenAcquireResultOk.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/EntraTokenAcquireResultOk.java deleted file mode 100644 index 3aeb59e1ef..0000000000 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/EntraTokenAcquireResultOk.java +++ /dev/null @@ -1,51 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - *--------------------------------------------------------------------------------------------*/ - -// AUTO-GENERATED FILE - DO NOT EDIT -// Generated from: api.schema.json - -package com.github.copilot.generated.rpc; - -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.annotation.JsonProperty; -import javax.annotation.processing.Generated; - -/** - * Variant {@code ok} of {@link EntraTokenAcquireResult}. - * - * @since 1.0.0 - */ -@JsonIgnoreProperties(ignoreUnknown = true) -@JsonInclude(JsonInclude.Include.NON_NULL) -@javax.annotation.processing.Generated("copilot-sdk-codegen") -final class EntraTokenAcquireResultOk extends EntraTokenAcquireResult { - - @JsonProperty("status") - private final String status = "ok"; - - @Override - public String getStatus() { return status; } - - /** Opaque access token. */ - @JsonProperty("accessToken") - private String accessToken; - - /** Expiry as milliseconds since Unix epoch, when supplied by OneAuth. */ - @JsonProperty("expiresOnTimestamp") - private Double expiresOnTimestamp; - - /** Opaque OneAuth account id, when supplied by the broker. */ - @JsonProperty("accountId") - private String accountId; - - public String getAccessToken() { return accessToken; } - public void setAccessToken(String accessToken) { this.accessToken = accessToken; } - - public Double getExpiresOnTimestamp() { return expiresOnTimestamp; } - public void setExpiresOnTimestamp(Double expiresOnTimestamp) { this.expiresOnTimestamp = expiresOnTimestamp; } - - public String getAccountId() { return accountId; } - public void setAccountId(String accountId) { this.accountId = accountId; } -} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ModelArtifactDetails.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ModelArtifactDetails.java new file mode 100644 index 0000000000..cef8cb97cc --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ModelArtifactDetails.java @@ -0,0 +1,44 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.github.copilot.CopilotExperimental; +import java.util.List; +import javax.annotation.processing.Generated; + +/** + * Provider-reported model artifact metadata. + * + * @apiNote This type is experimental and may change in a future version. + * + * @since 1.0.0 + */ +@CopilotExperimental +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record ModelArtifactDetails( + /** Artifact format, such as `gguf`. */ + @JsonProperty("format") String format, + /** Primary model family. */ + @JsonProperty("family") String family, + /** Provider-reported model families. */ + @JsonProperty("families") List families, + /** Provider-reported parameter count label. */ + @JsonProperty("parameterSize") String parameterSize, + /** Provider-reported quantization label. */ + @JsonProperty("quantization") String quantization, + /** Provider-reported model architecture. */ + @JsonProperty("architecture") String architecture, + /** Provider-reported tokenizer. */ + @JsonProperty("tokenizer") String tokenizer +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ModelCapabilitiesSupports.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ModelCapabilitiesSupports.java index daaef4fcb4..17b9732e4d 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ModelCapabilitiesSupports.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ModelCapabilitiesSupports.java @@ -21,6 +21,8 @@ @JsonInclude(JsonInclude.Include.NON_NULL) @JsonIgnoreProperties(ignoreUnknown = true) public record ModelCapabilitiesSupports( + /** Whether the model supports provider-native thinking. Independent of configurable reasoning effort; omission means unknown. */ + @JsonProperty("thinking") Boolean thinking, /** Whether this model supports vision/image input */ @JsonProperty("vision") Boolean vision, /** Whether this model supports canonical tool calling */ diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ModelProviderAdapterDescriptor.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ModelProviderAdapterDescriptor.java new file mode 100644 index 0000000000..1491a834d3 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ModelProviderAdapterDescriptor.java @@ -0,0 +1,42 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.github.copilot.CopilotExperimental; +import java.util.List; +import javax.annotation.processing.Generated; + +/** + * A normalized model-provider adapter in the session's effective catalog. + * + * @apiNote This type is experimental and may change in a future version. + * + * @since 1.0.0 + */ +@CopilotExperimental +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record ModelProviderAdapterDescriptor( + /** Stable opaque identity for routing to this adapter. Unique in the effective catalog, independent of live registration generations. */ + @JsonProperty("adapterId") String adapterId, + /** Descriptive provider family, such as `ollama`. Different adapters may have the same family; use adapterId for routing. */ + @JsonProperty("providerKind") String providerKind, + /** Human-readable provider name. */ + @JsonProperty("displayName") String displayName, + /** Attribution for the adapter itself. */ + @JsonProperty("provenance") ModelProviderAttribution provenance, + /** Adapter-declared policy for passive and automatic discovery. */ + @JsonProperty("automaticDiscovery") ModelProviderAutomaticDiscoveryPolicy automaticDiscovery, + /** Operations supported by this provider adapter. */ + @JsonProperty("operations") List operations +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ModelProviderAdapterOperationDescriptor.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ModelProviderAdapterOperationDescriptor.java new file mode 100644 index 0000000000..b859352e1b --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ModelProviderAdapterOperationDescriptor.java @@ -0,0 +1,33 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.github.copilot.CopilotExperimental; +import javax.annotation.processing.Generated; + +/** + * An operation supported by a model-provider adapter. + * + * @apiNote This type is experimental and may change in a future version. + * + * @since 1.0.0 + */ +@CopilotExperimental +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record ModelProviderAdapterOperationDescriptor( + /** Supported operation name: `discover`, `getStatus`, or `models.list`. Unknown names and duplicate declarations are rejected. */ + @JsonProperty("name") String name, + /** Optional self-contained JSON Schema Draft 7 for non-null discovery input. Only supported on discover. No external references are resolved. Omitted or null input selects defaults when requiresInput is false. Without a schema, the adapter validates supplied input. */ + @JsonProperty("inputSchema") Object inputSchema +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ModelProviderAttribution.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ModelProviderAttribution.java new file mode 100644 index 0000000000..74072a9f16 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ModelProviderAttribution.java @@ -0,0 +1,35 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.github.copilot.CopilotExperimental; +import javax.annotation.processing.Generated; + +/** + * Contributor attribution, independent of routing identity and authorization. + * + * @apiNote This type is experimental and may change in a future version. + * + * @since 1.0.0 + */ +@CopilotExperimental +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record ModelProviderAttribution( + /** Kind of component that supplied the adapter. Attribution does not confer authority. */ + @JsonProperty("source") ModelProviderProvenanceSource source, + /** Stable contributor identifier. Required and nonblank for extension and custom sources; optional for built-in and configured sources. Does not grant authority. */ + @JsonProperty("ownerId") String ownerId, + /** Human-readable contributor name, not the adapter display name. */ + @JsonProperty("ownerDisplayName") String ownerDisplayName +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ModelProviderAutomaticDiscoveryMode.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ModelProviderAutomaticDiscoveryMode.java new file mode 100644 index 0000000000..1907b5a2a2 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ModelProviderAutomaticDiscoveryMode.java @@ -0,0 +1,37 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.generated.rpc; + +import javax.annotation.processing.Generated; + +/** + * When the runtime may run an adapter without an explicit user action. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public enum ModelProviderAutomaticDiscoveryMode { + /** The {@code automatic} variant. */ + AUTOMATIC("automatic"), + /** The {@code configuredOnly} variant. */ + CONFIGUREDONLY("configuredOnly"), + /** The {@code explicit} variant. */ + EXPLICIT("explicit"); + + private final String value; + ModelProviderAutomaticDiscoveryMode(String value) { this.value = value; } + @com.fasterxml.jackson.annotation.JsonValue + public String getValue() { return value; } + @com.fasterxml.jackson.annotation.JsonCreator + public static ModelProviderAutomaticDiscoveryMode fromValue(String value) { + for (ModelProviderAutomaticDiscoveryMode v : values()) { + if (v.value.equals(value)) return v; + } + throw new IllegalArgumentException("Unknown ModelProviderAutomaticDiscoveryMode value: " + value); + } +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ModelProviderAutomaticDiscoveryPolicy.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ModelProviderAutomaticDiscoveryPolicy.java new file mode 100644 index 0000000000..f7fd039063 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ModelProviderAutomaticDiscoveryPolicy.java @@ -0,0 +1,37 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.github.copilot.CopilotExperimental; +import javax.annotation.processing.Generated; + +/** + * Adapter-declared policy that tells clients whether discovery may run automatically. + * + * @apiNote This type is experimental and may change in a future version. + * + * @since 1.0.0 + */ +@CopilotExperimental +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record ModelProviderAutomaticDiscoveryPolicy( + /** Whether automatic discovery is allowed, limited to configured providers, or explicit-only. */ + @JsonProperty("mode") ModelProviderAutomaticDiscoveryMode mode, + /** Maximum network scope used by this adapter during discovery. */ + @JsonProperty("networkScope") ModelProviderDiscoveryNetworkScope networkScope, + /** True when discovery requires non-null caller input. Omission or null is rejected before adapter execution. When false, omitted or null input selects adapter defaults without schema validation. */ + @JsonProperty("requiresInput") Boolean requiresInput, + /** True when the adapter must be enabled by a trusted owner, such as a trusted extension, before automatic discovery may run. */ + @JsonProperty("requiresTrust") Boolean requiresTrust +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ModelProviderConfigurationDisposition.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ModelProviderConfigurationDisposition.java new file mode 100644 index 0000000000..4c8b078fc4 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ModelProviderConfigurationDisposition.java @@ -0,0 +1,35 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.generated.rpc; + +import javax.annotation.processing.Generated; + +/** + * Whether a planned configuration entry is new or already present in the session registry. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public enum ModelProviderConfigurationDisposition { + /** The {@code create} variant. */ + CREATE("create"), + /** The {@code alreadyConfigured} variant. */ + ALREADYCONFIGURED("alreadyConfigured"); + + private final String value; + ModelProviderConfigurationDisposition(String value) { this.value = value; } + @com.fasterxml.jackson.annotation.JsonValue + public String getValue() { return value; } + @com.fasterxml.jackson.annotation.JsonCreator + public static ModelProviderConfigurationDisposition fromValue(String value) { + for (ModelProviderConfigurationDisposition v : values()) { + if (v.value.equals(value)) return v; + } + throw new IllegalArgumentException("Unknown ModelProviderConfigurationDisposition value: " + value); + } +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ModelProviderDiscoveryNetworkScope.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ModelProviderDiscoveryNetworkScope.java new file mode 100644 index 0000000000..30d889ee9b --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ModelProviderDiscoveryNetworkScope.java @@ -0,0 +1,41 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.generated.rpc; + +import javax.annotation.processing.Generated; + +/** + * Network reach an adapter may use during discovery. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public enum ModelProviderDiscoveryNetworkScope { + /** The {@code none} variant. */ + NONE("none"), + /** The {@code loopbackOnly} variant. */ + LOOPBACKONLY("loopbackOnly"), + /** The {@code configuredEndpointOnly} variant. */ + CONFIGUREDENDPOINTONLY("configuredEndpointOnly"), + /** The {@code localNetwork} variant. */ + LOCALNETWORK("localNetwork"), + /** The {@code internet} variant. */ + INTERNET("internet"); + + private final String value; + ModelProviderDiscoveryNetworkScope(String value) { this.value = value; } + @com.fasterxml.jackson.annotation.JsonValue + public String getValue() { return value; } + @com.fasterxml.jackson.annotation.JsonCreator + public static ModelProviderDiscoveryNetworkScope fromValue(String value) { + for (ModelProviderDiscoveryNetworkScope v : values()) { + if (v.value.equals(value)) return v; + } + throw new IllegalArgumentException("Unknown ModelProviderDiscoveryNetworkScope value: " + value); + } +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ModelProviderInstance.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ModelProviderInstance.java new file mode 100644 index 0000000000..06db8b4f3d --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ModelProviderInstance.java @@ -0,0 +1,43 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.github.copilot.CopilotExperimental; +import javax.annotation.processing.Generated; + +/** + * A normalized model-provider instance discovered by the runtime. + * + * @apiNote This type is experimental and may change in a future version. + * + * @since 1.0.0 + */ +@CopilotExperimental +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record ModelProviderInstance( + /** Self-contained reference for subsequent provider operations. */ + @JsonProperty("reference") ModelProviderInstanceReference reference, + /** Human-readable instance name. */ + @JsonProperty("displayName") String displayName, + /** Attribution for the adapter that produced this instance. */ + @JsonProperty("provenance") ModelProviderProvenance provenance, + /** Inference API endpoint when the provider exposes one separately from its management endpoint. */ + @JsonProperty("inferenceEndpoint") String inferenceEndpoint, + /** Provider family to use for inference against this instance. */ + @JsonProperty("inferenceType") ProviderEndpointType inferenceType, + /** Wire API to use for inference against this instance, when required by the provider family. */ + @JsonProperty("inferenceWireApi") ProviderEndpointWireApi inferenceWireApi, + /** Transport to use for inference against this instance. */ + @JsonProperty("inferenceTransport") ProviderEndpointTransport inferenceTransport +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ModelProviderInstanceReference.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ModelProviderInstanceReference.java new file mode 100644 index 0000000000..621c26af9d --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ModelProviderInstanceReference.java @@ -0,0 +1,37 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.github.copilot.CopilotExperimental; +import javax.annotation.processing.Generated; + +/** + * Serializable reference to a discovered provider instance. + * + * @apiNote This type is experimental and may change in a future version. + * + * @since 1.0.0 + */ +@CopilotExperimental +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record ModelProviderInstanceReference( + /** Stable opaque identity of the adapter that owns this reference. Must be present in the target session's effective catalog. */ + @JsonProperty("adapterId") String adapterId, + /** Descriptive provider family. Must match the selected adapter; not a routing key. */ + @JsonProperty("providerKind") String providerKind, + /** Stable instance identifier derived by the provider adapter, such as `ollama:{normalizedEndpoint}`. */ + @JsonProperty("id") String id, + /** Absolute provider management URI. The adapter validates normalization, supported schemes, and permission to access it against its bound configuration; a reference does not grant authority. */ + @JsonProperty("managementEndpoint") String managementEndpoint +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ModelProviderOperationOutcome.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ModelProviderOperationOutcome.java new file mode 100644 index 0000000000..6e10eddd97 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ModelProviderOperationOutcome.java @@ -0,0 +1,33 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.github.copilot.CopilotExperimental; +import javax.annotation.processing.Generated; + +/** + * Typed provider-operation outcome. Use the code for control flow and the optional message for display. + * + * @apiNote This type is experimental and may change in a future version. + * + * @since 1.0.0 + */ +@CopilotExperimental +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record ModelProviderOperationOutcome( + /** Machine-readable operation outcome. */ + @JsonProperty("code") ModelProviderOperationOutcomeCode code, + /** Human-readable detail for non-success outcomes. */ + @JsonProperty("message") String message +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ModelProviderOperationOutcomeCode.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ModelProviderOperationOutcomeCode.java new file mode 100644 index 0000000000..feab7ef5b8 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ModelProviderOperationOutcomeCode.java @@ -0,0 +1,39 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.generated.rpc; + +import javax.annotation.processing.Generated; + +/** + * Typed outcome for a provider operation. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public enum ModelProviderOperationOutcomeCode { + /** The {@code success} variant. */ + SUCCESS("success"), + /** The {@code absent} variant. */ + ABSENT("absent"), + /** The {@code unreachable} variant. */ + UNREACHABLE("unreachable"), + /** The {@code failed} variant. */ + FAILED("failed"); + + private final String value; + ModelProviderOperationOutcomeCode(String value) { this.value = value; } + @com.fasterxml.jackson.annotation.JsonValue + public String getValue() { return value; } + @com.fasterxml.jackson.annotation.JsonCreator + public static ModelProviderOperationOutcomeCode fromValue(String value) { + for (ModelProviderOperationOutcomeCode v : values()) { + if (v.value.equals(value)) return v; + } + throw new IllegalArgumentException("Unknown ModelProviderOperationOutcomeCode value: " + value); + } +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ModelProviderProvenance.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ModelProviderProvenance.java new file mode 100644 index 0000000000..186a32c91f --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ModelProviderProvenance.java @@ -0,0 +1,39 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.github.copilot.CopilotExperimental; +import javax.annotation.processing.Generated; + +/** + * Attribution for the adapter that produced a provider row. + * + * @apiNote This type is experimental and may change in a future version. + * + * @since 1.0.0 + */ +@CopilotExperimental +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record ModelProviderProvenance( + /** Stable opaque adapter identity from the effective catalog. Treat this as a whole identifier, not a parseable owner or kind. */ + @JsonProperty("adapterId") String adapterId, + /** Descriptive provider family that produced this row; not a routing key. */ + @JsonProperty("providerKind") String providerKind, + /** Kind of component that supplied the adapter. */ + @JsonProperty("source") ModelProviderProvenanceSource source, + /** Stable contributor identifier when the adapter has an owner outside the runtime. Independent of the contribution mechanism and not a routing key. */ + @JsonProperty("ownerId") String ownerId, + /** Human-readable contributor name, not the adapter display name. */ + @JsonProperty("ownerDisplayName") String ownerDisplayName +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ModelProviderProvenanceSource.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ModelProviderProvenanceSource.java new file mode 100644 index 0000000000..84e91393b9 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ModelProviderProvenanceSource.java @@ -0,0 +1,39 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.generated.rpc; + +import javax.annotation.processing.Generated; + +/** + * Kind of component that supplied a provider adapter or row. Attribution does not confer authority. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public enum ModelProviderProvenanceSource { + /** The {@code builtIn} variant. */ + BUILTIN("builtIn"), + /** The {@code configured} variant. */ + CONFIGURED("configured"), + /** The {@code extension} variant. */ + EXTENSION("extension"), + /** The {@code custom} variant. */ + CUSTOM("custom"); + + private final String value; + ModelProviderProvenanceSource(String value) { this.value = value; } + @com.fasterxml.jackson.annotation.JsonValue + public String getValue() { return value; } + @com.fasterxml.jackson.annotation.JsonCreator + public static ModelProviderProvenanceSource fromValue(String value) { + for (ModelProviderProvenanceSource v : values()) { + if (v.value.equals(value)) return v; + } + throw new IllegalArgumentException("Unknown ModelProviderProvenanceSource value: " + value); + } +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ModelProviderWarning.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ModelProviderWarning.java new file mode 100644 index 0000000000..deb6f08c78 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ModelProviderWarning.java @@ -0,0 +1,33 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.github.copilot.CopilotExperimental; +import javax.annotation.processing.Generated; + +/** + * A non-fatal provider observation warning. + * + * @apiNote This type is experimental and may change in a future version. + * + * @since 1.0.0 + */ +@CopilotExperimental +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record ModelProviderWarning( + /** Machine-readable warning code. */ + @JsonProperty("code") String code, + /** Human-readable warning message. */ + @JsonProperty("message") String message +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SandboxProxyCaCreateParams.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SandboxProxyCaCreateParams.java new file mode 100644 index 0000000000..ac0872b397 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SandboxProxyCaCreateParams.java @@ -0,0 +1,30 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.github.copilot.CopilotExperimental; +import javax.annotation.processing.Generated; + +/** + * Identifies the credential hosts that the persistent certificate authority of the sandbox credential proxy must cover. The runtime always adds the hosts from the saved user settings. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ +@CopilotExperimental +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SandboxProxyCaCreateParams( + /** The sandbox configuration that the host gives its sessions. The runtime reads the credential hosts from `auth` and `credentials`; it ignores `enabled` and the other fields. */ + @JsonProperty("sandboxConfig") SandboxConfig sandboxConfig +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SandboxProxyCaCreateResult.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SandboxProxyCaCreateResult.java new file mode 100644 index 0000000000..a4d2f12cc2 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SandboxProxyCaCreateResult.java @@ -0,0 +1,30 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.github.copilot.CopilotExperimental; +import javax.annotation.processing.Generated; + +/** + * Result of creating the persistent certificate authority of the sandbox credential proxy. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ +@CopilotExperimental +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SandboxProxyCaCreateResult( + /** Absolute path of the public certificate of the certificate authority, in PEM format. */ + @JsonProperty("certificatePath") String certificatePath +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SandboxProxyCaGetStatusParams.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SandboxProxyCaGetStatusParams.java new file mode 100644 index 0000000000..6f654fb053 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SandboxProxyCaGetStatusParams.java @@ -0,0 +1,30 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.github.copilot.CopilotExperimental; +import javax.annotation.processing.Generated; + +/** + * Identifies the credential hosts that the persistent certificate authority of the sandbox credential proxy must cover. The runtime always adds the hosts from the saved user settings. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ +@CopilotExperimental +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SandboxProxyCaGetStatusParams( + /** The sandbox configuration that the host gives its sessions. The runtime reads the credential hosts from `auth` and `credentials`; it ignores `enabled` and the other fields. */ + @JsonProperty("sandboxConfig") SandboxConfig sandboxConfig +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SandboxProxyCaGetStatusResult.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SandboxProxyCaGetStatusResult.java new file mode 100644 index 0000000000..c01ac9077d --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SandboxProxyCaGetStatusResult.java @@ -0,0 +1,34 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.github.copilot.CopilotExperimental; +import javax.annotation.processing.Generated; + +/** + * Status of the persistent certificate authority of the sandbox credential proxy. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ +@CopilotExperimental +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SandboxProxyCaGetStatusResult( + /** The state of the certificate authority. */ + @JsonProperty("state") SandboxProxyCaState state, + /** Human-readable reason for the state. On `installed` or `notInstalled`, present only when the certificate authority must be rotated, and then says why. */ + @JsonProperty("detail") String detail, + /** Whether this process can add the certificate authority to OS trust without credentials from a different user. False where OS trust is unsupported, and on Windows when the process cannot elevate itself to write the machine trust store. When false, do not offer to set up the certificate authority. */ + @JsonProperty("canInstall") Boolean canInstall +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SandboxProxyCaRemoveResult.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SandboxProxyCaRemoveResult.java new file mode 100644 index 0000000000..8e6dd46d30 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SandboxProxyCaRemoveResult.java @@ -0,0 +1,34 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.github.copilot.CopilotExperimental; +import javax.annotation.processing.Generated; + +/** + * Status of the persistent certificate authority of the sandbox credential proxy. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ +@CopilotExperimental +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SandboxProxyCaRemoveResult( + /** The state of the certificate authority. */ + @JsonProperty("state") SandboxProxyCaState state, + /** Human-readable reason for the state. On `installed` or `notInstalled`, present only when the certificate authority must be rotated, and then says why. */ + @JsonProperty("detail") String detail, + /** Whether this process can add the certificate authority to OS trust without credentials from a different user. False where OS trust is unsupported, and on Windows when the process cannot elevate itself to write the machine trust store. When false, do not offer to set up the certificate authority. */ + @JsonProperty("canInstall") Boolean canInstall +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SandboxProxyCaRotateParams.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SandboxProxyCaRotateParams.java new file mode 100644 index 0000000000..2965bb3151 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SandboxProxyCaRotateParams.java @@ -0,0 +1,30 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.github.copilot.CopilotExperimental; +import javax.annotation.processing.Generated; + +/** + * Identifies the credential hosts that the persistent certificate authority of the sandbox credential proxy must cover. The runtime always adds the hosts from the saved user settings. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ +@CopilotExperimental +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SandboxProxyCaRotateParams( + /** The sandbox configuration that the host gives its sessions. The runtime reads the credential hosts from `auth` and `credentials`; it ignores `enabled` and the other fields. */ + @JsonProperty("sandboxConfig") SandboxConfig sandboxConfig +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SandboxProxyCaRotateResult.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SandboxProxyCaRotateResult.java new file mode 100644 index 0000000000..b53cc5aa68 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SandboxProxyCaRotateResult.java @@ -0,0 +1,34 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.github.copilot.CopilotExperimental; +import javax.annotation.processing.Generated; + +/** + * Status of the persistent certificate authority of the sandbox credential proxy. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ +@CopilotExperimental +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SandboxProxyCaRotateResult( + /** The state of the certificate authority. */ + @JsonProperty("state") SandboxProxyCaState state, + /** Human-readable reason for the state. On `installed` or `notInstalled`, present only when the certificate authority must be rotated, and then says why. */ + @JsonProperty("detail") String detail, + /** Whether this process can add the certificate authority to OS trust without credentials from a different user. False where OS trust is unsupported, and on Windows when the process cannot elevate itself to write the machine trust store. When false, do not offer to set up the certificate authority. */ + @JsonProperty("canInstall") Boolean canInstall +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/EntraTokenInteraction.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SandboxProxyCaState.java similarity index 57% rename from java/sdk/src/generated/java/com/github/copilot/generated/rpc/EntraTokenInteraction.java rename to java/sdk/src/generated/java/com/github/copilot/generated/rpc/SandboxProxyCaState.java index f6e78918dc..4a4dc15662 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/EntraTokenInteraction.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SandboxProxyCaState.java @@ -11,7 +11,7 @@ import javax.annotation.processing.Generated; /** - * How far OneAuth may go to acquire the requested token. + * State of the persistent certificate authority of the sandbox credential proxy. * * @apiNote This type is experimental and may change in a future version. * @@ -19,23 +19,25 @@ */ @CopilotExperimental @javax.annotation.processing.Generated("copilot-sdk-codegen") -enum EntraTokenInteraction { - /** The {@code silent} variant. */ - SILENT("silent"), - /** The {@code interactive} variant. */ - INTERACTIVE("interactive"), - /** The {@code force-interactive} variant. */ - FORCE_INTERACTIVE("force-interactive"); +public enum SandboxProxyCaState { + /** The {@code unsupported} variant. */ + UNSUPPORTED("unsupported"), + /** The {@code notInstalled} variant. */ + NOTINSTALLED("notInstalled"), + /** The {@code installed} variant. */ + INSTALLED("installed"), + /** The {@code error} variant. */ + ERROR("error"); private final String value; - EntraTokenInteraction(String value) { this.value = value; } + SandboxProxyCaState(String value) { this.value = value; } @com.fasterxml.jackson.annotation.JsonValue public String getValue() { return value; } @com.fasterxml.jackson.annotation.JsonCreator - public static EntraTokenInteraction fromValue(String value) { - for (EntraTokenInteraction v : values()) { + public static SandboxProxyCaState fromValue(String value) { + for (SandboxProxyCaState v : values()) { if (v.value.equals(value)) return v; } - throw new IllegalArgumentException("Unknown EntraTokenInteraction value: " + value); + throw new IllegalArgumentException("Unknown SandboxProxyCaState value: " + value); } } diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SandboxProxyCaTrustParams.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SandboxProxyCaTrustParams.java new file mode 100644 index 0000000000..b54e2ce93e --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SandboxProxyCaTrustParams.java @@ -0,0 +1,30 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.github.copilot.CopilotExperimental; +import javax.annotation.processing.Generated; + +/** + * Identifies the credential hosts that the persistent certificate authority of the sandbox credential proxy must cover. The runtime always adds the hosts from the saved user settings. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ +@CopilotExperimental +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SandboxProxyCaTrustParams( + /** The sandbox configuration that the host gives its sessions. The runtime reads the credential hosts from `auth` and `credentials`; it ignores `enabled` and the other fields. */ + @JsonProperty("sandboxConfig") SandboxConfig sandboxConfig +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SandboxProxyCaTrustResult.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SandboxProxyCaTrustResult.java new file mode 100644 index 0000000000..24e462e16b --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SandboxProxyCaTrustResult.java @@ -0,0 +1,34 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.github.copilot.CopilotExperimental; +import javax.annotation.processing.Generated; + +/** + * Status of the persistent certificate authority of the sandbox credential proxy. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ +@CopilotExperimental +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SandboxProxyCaTrustResult( + /** The state of the certificate authority. */ + @JsonProperty("state") SandboxProxyCaState state, + /** Human-readable reason for the state. On `installed` or `notInstalled`, present only when the certificate authority must be rotated, and then says why. */ + @JsonProperty("detail") String detail, + /** Whether this process can add the certificate authority to OS trust without credentials from a different user. False where OS trust is unsupported, and on Windows when the process cannot elevate itself to write the machine trust store. When false, do not offer to set up the certificate authority. */ + @JsonProperty("canInstall") Boolean canInstall +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerAccountsApi.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerAccountsApi.java deleted file mode 100644 index c920b38b05..0000000000 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerAccountsApi.java +++ /dev/null @@ -1,40 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - *--------------------------------------------------------------------------------------------*/ - -// AUTO-GENERATED FILE - DO NOT EDIT -// Generated from: api.schema.json - -package com.github.copilot.generated.rpc; - -import com.github.copilot.CopilotExperimental; -import java.util.concurrent.CompletableFuture; -import javax.annotation.processing.Generated; - -/** - * API methods for the {@code accounts} namespace. - * - * @since 1.0.0 - */ -@javax.annotation.processing.Generated("copilot-sdk-codegen") -final class ServerAccountsApi { - - private final RpcCaller caller; - - /** @param caller the RPC transport function */ - ServerAccountsApi(RpcCaller caller) { - this.caller = caller; - } - - /** - * OneAuth token request supplied by a trusted host application. - * - * @apiNote This method is experimental and may change in a future version. - * @since 1.0.0 - */ - @CopilotExperimental - CompletableFuture acquireEntraToken(AccountsAcquireEntraTokenParams params) { - return caller.invoke("accounts.acquireEntraToken", params, EntraTokenAcquireResult.class); - } - -} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerRpc.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerRpc.java index ad2e70cb0b..7eb9aa0118 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerRpc.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerRpc.java @@ -71,8 +71,6 @@ public final class ServerRpc { public final ServerSessionsApi sessions; /** API methods for the {@code agentRegistry} namespace. */ public final ServerAgentRegistryApi agentRegistry; - /** API methods for the {@code accounts} namespace. */ - final ServerAccountsApi accounts; /** * Creates a new server RPC client. @@ -104,7 +102,6 @@ public ServerRpc(RpcCaller caller) { this.llmInference = new ServerLlmInferenceApi(caller); this.sessions = new ServerSessionsApi(caller); this.agentRegistry = new ServerAgentRegistryApi(caller); - this.accounts = new ServerAccountsApi(caller); } /** diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerSandboxApi.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerSandboxApi.java index 9c33cda35c..2634955e4a 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerSandboxApi.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerSandboxApi.java @@ -21,9 +21,13 @@ public final class ServerSandboxApi { private final RpcCaller caller; + /** API methods for the {@code sandbox.proxyCa} sub-namespace. */ + public final ServerSandboxProxyCaApi proxyCa; + /** @param caller the RPC transport function */ ServerSandboxApi(RpcCaller caller) { this.caller = caller; + this.proxyCa = new ServerSandboxProxyCaApi(caller); } /** diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerSandboxProxyCaApi.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerSandboxProxyCaApi.java new file mode 100644 index 0000000000..0ffe1733d5 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerSandboxProxyCaApi.java @@ -0,0 +1,84 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.generated.rpc; + +import com.github.copilot.CopilotExperimental; +import java.util.concurrent.CompletableFuture; +import javax.annotation.processing.Generated; + +/** + * API methods for the {@code sandbox.proxyCa} namespace. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public final class ServerSandboxProxyCaApi { + + private final RpcCaller caller; + + /** @param caller the RPC transport function */ + ServerSandboxProxyCaApi(RpcCaller caller) { + this.caller = caller; + } + + /** + * Identifies the credential hosts that the persistent certificate authority of the sandbox credential proxy must cover. The runtime always adds the hosts from the saved user settings. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ + @CopilotExperimental + public CompletableFuture getStatus(SandboxProxyCaGetStatusParams params) { + return caller.invoke("sandbox.proxyCa.getStatus", params, SandboxProxyCaGetStatusResult.class); + } + + /** + * Identifies the credential hosts that the persistent certificate authority of the sandbox credential proxy must cover. The runtime always adds the hosts from the saved user settings. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ + @CopilotExperimental + public CompletableFuture create(SandboxProxyCaCreateParams params) { + return caller.invoke("sandbox.proxyCa.create", params, SandboxProxyCaCreateResult.class); + } + + /** + * Identifies the credential hosts that the persistent certificate authority of the sandbox credential proxy must cover. The runtime always adds the hosts from the saved user settings. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ + @CopilotExperimental + public CompletableFuture rotate(SandboxProxyCaRotateParams params) { + return caller.invoke("sandbox.proxyCa.rotate", params, SandboxProxyCaRotateResult.class); + } + + /** + * Identifies the credential hosts that the persistent certificate authority of the sandbox credential proxy must cover. The runtime always adds the hosts from the saved user settings. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ + @CopilotExperimental + public CompletableFuture trust(SandboxProxyCaTrustParams params) { + return caller.invoke("sandbox.proxyCa.trust", params, SandboxProxyCaTrustResult.class); + } + + /** + * Status of the persistent certificate authority of the sandbox credential proxy. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ + @CopilotExperimental + public CompletableFuture remove() { + return caller.invoke("sandbox.proxyCa.remove", java.util.Map.of(), SandboxProxyCaRemoveResult.class); + } + +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionCustomizationsApi.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionCustomizationsApi.java index 18d27db9b5..ee9134f803 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionCustomizationsApi.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionCustomizationsApi.java @@ -35,8 +35,8 @@ public final class SessionCustomizationsApi { * @since 1.0.0 */ @CopilotExperimental - public CompletableFuture reload() { - return caller.invoke("session.customizations.reload", java.util.Map.of("sessionId", this.sessionId), SessionCustomizationsReloadResult.class); + public CompletableFuture reload() { + return caller.invoke("session.customizations.reload", java.util.Map.of("sessionId", this.sessionId), CustomizationsReloadResult.class); } } diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionFsError.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionFsError.java index 349114dfde..0170b7f3b0 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionFsError.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionFsError.java @@ -24,6 +24,8 @@ public record SessionFsError( /** Error classification */ @JsonProperty("code") SessionFsErrorCode code, /** Free-form detail about the error, for logging/diagnostics */ - @JsonProperty("message") String message + @JsonProperty("message") String message, + /** For failed writeFile requests only: true if the provider changed the target before failing. Omit when unknown or unchanged. */ + @JsonProperty("writeChanged") Boolean writeChanged ) { } diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionFsReadFileBytesParams.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionFsReadFileBytesParams.java new file mode 100644 index 0000000000..9e89b0c1a2 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionFsReadFileBytesParams.java @@ -0,0 +1,32 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.github.copilot.CopilotExperimental; +import javax.annotation.processing.Generated; + +/** + * Path of the binary file to read from the client-provided session filesystem. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ +@CopilotExperimental +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionFsReadFileBytesParams( + /** Target session identifier */ + @JsonProperty("sessionId") String sessionId, + /** Path using SessionFs conventions */ + @JsonProperty("path") String path +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionFsReadFileBytesResult.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionFsReadFileBytesResult.java new file mode 100644 index 0000000000..f70a78c72f --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionFsReadFileBytesResult.java @@ -0,0 +1,32 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.github.copilot.CopilotExperimental; +import javax.annotation.processing.Generated; + +/** + * File bytes as standard base64, or a filesystem error if the read failed. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ +@CopilotExperimental +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionFsReadFileBytesResult( + /** Exact file bytes encoded as standard base64 */ + @JsonProperty("content") String content, + /** Describes a filesystem error. */ + @JsonProperty("error") SessionFsError error +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionFsSetProviderCapabilities.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionFsSetProviderCapabilities.java index 7d6c0adb39..f7d047c273 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionFsSetProviderCapabilities.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionFsSetProviderCapabilities.java @@ -22,6 +22,19 @@ @JsonIgnoreProperties(ignoreUnknown = true) public record SessionFsSetProviderCapabilities( /** Whether the provider supports SQLite query/exists operations */ - @JsonProperty("sqlite") Boolean sqlite + @JsonProperty("sqlite") Boolean sqlite, + /** Whether the provider supports binary reads and writes through sessionFs.readFileBytes and sessionFs.writeFileBytes */ + @JsonProperty("binary") Boolean binary ) { + + /** + * Creates provider capabilities without binary reads. + * + * @param sqlite Whether the provider supports SQLite query/exists operations + */ + public SessionFsSetProviderCapabilities( + Boolean sqlite + ) { + this(sqlite, null); + } } diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionFsWriteFileBytesParams.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionFsWriteFileBytesParams.java new file mode 100644 index 0000000000..2d095f371d --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionFsWriteFileBytesParams.java @@ -0,0 +1,36 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.github.copilot.CopilotExperimental; +import javax.annotation.processing.Generated; + +/** + * File path, standard-base64-encoded bytes to write, and optional mode for the client-provided session filesystem. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ +@CopilotExperimental +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionFsWriteFileBytesParams( + /** Target session identifier */ + @JsonProperty("sessionId") String sessionId, + /** Path using SessionFs conventions */ + @JsonProperty("path") String path, + /** Exact file bytes encoded as standard base64 */ + @JsonProperty("content") String content, + /** Optional POSIX-style mode for newly created files */ + @JsonProperty("mode") Long mode +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionInstructionsApi.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionInstructionsApi.java index d5971f70fe..0a1cf9ba21 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionInstructionsApi.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionInstructionsApi.java @@ -46,8 +46,8 @@ public CompletableFuture getSources() { * @since 1.0.0 */ @CopilotExperimental - public CompletableFuture reload() { - return caller.invoke("session.instructions.reload", java.util.Map.of("sessionId", this.sessionId), Void.class); + public CompletableFuture reload() { + return caller.invoke("session.instructions.reload", java.util.Map.of("sessionId", this.sessionId), SessionInstructionsGetSourcesResult.class); } } diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionModelGetCurrentResult.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionModelGetCurrentResult.java index 23a9788540..6d6e79a26a 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionModelGetCurrentResult.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionModelGetCurrentResult.java @@ -26,6 +26,8 @@ public record SessionModelGetCurrentResult( /** Currently active model identifier */ @JsonProperty("modelId") String modelId, + /** Captured base model to restore when leaving plan mode. Omitted outside plan mode or when no plan override has captured a base model. Persistent agent model requirements apply to this model rather than the temporary plan model. */ + @JsonProperty("planBaseModelId") String planBaseModelId, /** Reasoning effort level currently applied to the active model, when one is set. Reads `Session.getReasoningEffort()` synchronously after `getSelectedModel()` resolves so the two values are reported as a snapshot. */ @JsonProperty("reasoningEffort") String reasoningEffort, /** Context tier for models that support multiple context-window sizes. */ diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionProvidersApi.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionProvidersApi.java new file mode 100644 index 0000000000..6f6dc8b5e5 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionProvidersApi.java @@ -0,0 +1,80 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.generated.rpc; + +import com.github.copilot.CopilotExperimental; +import java.util.concurrent.CompletableFuture; +import javax.annotation.processing.Generated; + +/** + * API methods for the {@code providers} namespace. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public final class SessionProvidersApi { + + private static final com.fasterxml.jackson.databind.ObjectMapper MAPPER = RpcMapper.INSTANCE; + + private final RpcCaller caller; + private final String sessionId; + + /** API methods for the {@code providers.models} sub-namespace. */ + public final SessionProvidersModelsApi models; + + /** @param caller the RPC transport function */ + SessionProvidersApi(RpcCaller caller, String sessionId) { + this.caller = caller; + this.sessionId = sessionId; + this.models = new SessionProvidersModelsApi(caller, sessionId); + } + + /** + * Identifies the target session. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ + @CopilotExperimental + public CompletableFuture getCatalog() { + return caller.invoke("session.providers.getCatalog", java.util.Map.of("sessionId", this.sessionId), SessionProvidersGetCatalogResult.class); + } + + /** + * Provider discovery parameters. + *

+ * Note: the {@code sessionId} field in the params record is overridden + * by the session-scoped wrapper; any value provided is ignored. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ + @CopilotExperimental + public CompletableFuture discover(SessionProvidersDiscoverParams params) { + com.fasterxml.jackson.databind.node.ObjectNode _p = MAPPER.valueToTree(params); + _p.put("sessionId", this.sessionId); + return caller.invoke("session.providers.discover", _p, SessionProvidersDiscoverResult.class); + } + + /** + * Provider status request parameters. + *

+ * Note: the {@code sessionId} field in the params record is overridden + * by the session-scoped wrapper; any value provided is ignored. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ + @CopilotExperimental + public CompletableFuture getStatus(SessionProvidersGetStatusParams params) { + com.fasterxml.jackson.databind.node.ObjectNode _p = MAPPER.valueToTree(params); + _p.put("sessionId", this.sessionId); + return caller.invoke("session.providers.getStatus", _p, SessionProvidersGetStatusResult.class); + } + +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionProvidersDiscoverParams.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionProvidersDiscoverParams.java new file mode 100644 index 0000000000..f5400285f8 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionProvidersDiscoverParams.java @@ -0,0 +1,34 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.github.copilot.CopilotExperimental; +import javax.annotation.processing.Generated; + +/** + * Provider discovery parameters. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ +@CopilotExperimental +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionProvidersDiscoverParams( + /** Target session identifier */ + @JsonProperty("sessionId") String sessionId, + /** Opaque adapter identity returned by `session.providers.getCatalog`. */ + @JsonProperty("adapterId") String adapterId, + /** Provider-specific JSON input. Omission or null selects adapter defaults unless requiresInput is true. Non-null input is validated against the advertised Draft 7 schema when present; otherwise validation belongs to the adapter. */ + @JsonProperty("input") Object input +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionProvidersDiscoverResult.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionProvidersDiscoverResult.java new file mode 100644 index 0000000000..1467adc6a9 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionProvidersDiscoverResult.java @@ -0,0 +1,33 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.github.copilot.CopilotExperimental; +import java.util.List; +import javax.annotation.processing.Generated; + +/** + * Provider instances found by a discovery operation. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ +@CopilotExperimental +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionProvidersDiscoverResult( + /** Typed operation outcome. Passive discovery can return `absent` with an empty instance list. */ + @JsonProperty("outcome") ModelProviderOperationOutcome outcome, + /** Discovered provider instances. Empty when passive default discovery finds no reachable provider. */ + @JsonProperty("instances") List instances +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/EntraTokenAcquireResultInteractionRequired.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionProvidersGetCatalogParams.java similarity index 70% rename from java/sdk/src/generated/java/com/github/copilot/generated/rpc/EntraTokenAcquireResultInteractionRequired.java rename to java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionProvidersGetCatalogParams.java index cc05e67fe3..1fd343bb0a 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/EntraTokenAcquireResultInteractionRequired.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionProvidersGetCatalogParams.java @@ -10,21 +10,21 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; +import com.github.copilot.CopilotExperimental; import javax.annotation.processing.Generated; /** - * Variant {@code interaction-required} of {@link EntraTokenAcquireResult}. + * Identifies the target session. * + * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 */ -@JsonIgnoreProperties(ignoreUnknown = true) -@JsonInclude(JsonInclude.Include.NON_NULL) +@CopilotExperimental @javax.annotation.processing.Generated("copilot-sdk-codegen") -final class EntraTokenAcquireResultInteractionRequired extends EntraTokenAcquireResult { - - @JsonProperty("status") - private final String status = "interaction-required"; - - @Override - public String getStatus() { return status; } +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionProvidersGetCatalogParams( + /** Target session identifier */ + @JsonProperty("sessionId") String sessionId +) { } diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionCustomizationsReloadResult.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionProvidersGetCatalogResult.java similarity index 68% rename from java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionCustomizationsReloadResult.java rename to java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionProvidersGetCatalogResult.java index 5f6fa34306..49c23ba195 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionCustomizationsReloadResult.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionProvidersGetCatalogResult.java @@ -15,7 +15,7 @@ import javax.annotation.processing.Generated; /** - * Diagnostics from reloading skill definitions, with warnings and errors as separate lists. + * Normalized model-provider adapter definitions available to the session, not discovered instances. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -24,10 +24,8 @@ @javax.annotation.processing.Generated("copilot-sdk-codegen") @JsonInclude(JsonInclude.Include.NON_NULL) @JsonIgnoreProperties(ignoreUnknown = true) -public record SessionCustomizationsReloadResult( - /** Warnings emitted while loading skills (e.g. skills that loaded but had issues) */ - @JsonProperty("warnings") List warnings, - /** Errors emitted while loading skills (e.g. skills that failed to load entirely) */ - @JsonProperty("errors") List errors +public record SessionProvidersGetCatalogResult( + /** Available provider adapters ordered by adapterId. */ + @JsonProperty("providers") List providers ) { } diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionProvidersGetStatusParams.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionProvidersGetStatusParams.java new file mode 100644 index 0000000000..56e686dd30 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionProvidersGetStatusParams.java @@ -0,0 +1,32 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.github.copilot.CopilotExperimental; +import javax.annotation.processing.Generated; + +/** + * Provider status request parameters. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ +@CopilotExperimental +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionProvidersGetStatusParams( + /** Target session identifier */ + @JsonProperty("sessionId") String sessionId, + /** Provider instance reference returned by discovery. */ + @JsonProperty("instance") ModelProviderInstanceReference instance +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionProvidersGetStatusResult.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionProvidersGetStatusResult.java new file mode 100644 index 0000000000..cff3c86ed8 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionProvidersGetStatusResult.java @@ -0,0 +1,36 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.github.copilot.CopilotExperimental; +import javax.annotation.processing.Generated; + +/** + * Current health information for a provider instance. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ +@CopilotExperimental +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionProvidersGetStatusResult( + /** Typed operation outcome. */ + @JsonProperty("outcome") ModelProviderOperationOutcome outcome, + /** Normalized provider instance. */ + @JsonProperty("instance") ModelProviderInstance instance, + /** Open provider status value, such as `healthy`, `unreachable`, or `notInstalled`. */ + @JsonProperty("status") String status, + /** Provider-reported version. */ + @JsonProperty("version") String version +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionProvidersModelsApi.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionProvidersModelsApi.java new file mode 100644 index 0000000000..b309fc7c12 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionProvidersModelsApi.java @@ -0,0 +1,65 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.generated.rpc; + +import com.github.copilot.CopilotExperimental; +import java.util.concurrent.CompletableFuture; +import javax.annotation.processing.Generated; + +/** + * API methods for the {@code providers.models} namespace. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public final class SessionProvidersModelsApi { + + private static final com.fasterxml.jackson.databind.ObjectMapper MAPPER = RpcMapper.INSTANCE; + + private final RpcCaller caller; + private final String sessionId; + + /** @param caller the RPC transport function */ + SessionProvidersModelsApi(RpcCaller caller, String sessionId) { + this.caller = caller; + this.sessionId = sessionId; + } + + /** + * Provider model inventory request parameters. + *

+ * Note: the {@code sessionId} field in the params record is overridden + * by the session-scoped wrapper; any value provided is ignored. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ + @CopilotExperimental + public CompletableFuture list(SessionProvidersModelsListParams params) { + com.fasterxml.jackson.databind.node.ObjectNode _p = MAPPER.valueToTree(params); + _p.put("sessionId", this.sessionId); + return caller.invoke("session.providers.models.list", _p, SessionProvidersModelsListResult.class); + } + + /** + * A discovered instance and one of its models to translate into provider configuration. Pass back the instance and model as returned by `session.providers.discover` and `session.providers.models.list`. + *

+ * Note: the {@code sessionId} field in the params record is overridden + * by the session-scoped wrapper; any value provided is ignored. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ + @CopilotExperimental + public CompletableFuture prepareConfiguration(SessionProvidersModelsPrepareConfigurationParams params) { + com.fasterxml.jackson.databind.node.ObjectNode _p = MAPPER.valueToTree(params); + _p.put("sessionId", this.sessionId); + return caller.invoke("session.providers.models.prepareConfiguration", _p, SessionProvidersModelsPrepareConfigurationResult.class); + } + +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionProvidersModelsListParams.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionProvidersModelsListParams.java new file mode 100644 index 0000000000..89f4984ae4 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionProvidersModelsListParams.java @@ -0,0 +1,32 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.github.copilot.CopilotExperimental; +import javax.annotation.processing.Generated; + +/** + * Provider model inventory request parameters. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ +@CopilotExperimental +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionProvidersModelsListParams( + /** Target session identifier */ + @JsonProperty("sessionId") String sessionId, + /** Provider instance reference returned by discovery. */ + @JsonProperty("instance") ModelProviderInstanceReference instance +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionProvidersModelsListResult.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionProvidersModelsListResult.java new file mode 100644 index 0000000000..307ee6728b --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionProvidersModelsListResult.java @@ -0,0 +1,33 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.github.copilot.CopilotExperimental; +import java.util.List; +import javax.annotation.processing.Generated; + +/** + * Models offered for agent conversations by one provider instance. Adapters exclude known-incompatible models, but retain candidates with unknown capabilities. Listing does not guarantee compatibility. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ +@CopilotExperimental +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionProvidersModelsListResult( + /** Typed operation outcome. */ + @JsonProperty("outcome") ModelProviderOperationOutcome outcome, + /** Provider-native models in provider order. */ + @JsonProperty("models") List models +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionProvidersModelsPrepareConfigurationParams.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionProvidersModelsPrepareConfigurationParams.java new file mode 100644 index 0000000000..60548516cb --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionProvidersModelsPrepareConfigurationParams.java @@ -0,0 +1,34 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.github.copilot.CopilotExperimental; +import javax.annotation.processing.Generated; + +/** + * A discovered instance and one of its models to translate into provider configuration. Pass back the instance and model as returned by `session.providers.discover` and `session.providers.models.list`. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ +@CopilotExperimental +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionProvidersModelsPrepareConfigurationParams( + /** Target session identifier */ + @JsonProperty("sessionId") String sessionId, + /** The discovered instance that serves the model. */ + @JsonProperty("instance") ModelProviderInstance instance, + /** The discovered model to configure. */ + @JsonProperty("model") DiscoveredModel model +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionProvidersModelsPrepareConfigurationResult.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionProvidersModelsPrepareConfigurationResult.java new file mode 100644 index 0000000000..e2f2abeebb --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionProvidersModelsPrepareConfigurationResult.java @@ -0,0 +1,41 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.github.copilot.CopilotExperimental; +import java.util.List; +import javax.annotation.processing.Generated; + +/** + * Provider configuration prepared from a discovered model. Preparing a plan changes nothing: it neither registers the model with the session nor writes durable configuration. To apply it, pass `provider` and `model` to `session.provider.add`, omitting whichever the dispositions report as already configured. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ +@CopilotExperimental +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionProvidersModelsPrepareConfigurationResult( + /** Provider connection prepared from the instance's inference metadata. Carries no credential; supply one if the endpoint requires it. */ + @JsonProperty("provider") NamedProviderConfig provider, + /** Model definition prepared from the discovered model. Capability fields the provider did not report stay omitted rather than being asserted false. */ + @JsonProperty("model") ProviderModelConfig model, + /** Provider-qualified selection id (`provider/id`) to pass to `switchTo` once the plan is applied. */ + @JsonProperty("selectionId") String selectionId, + /** Whether `provider` still needs to be registered. When `alreadyConfigured`, a provider with the same endpoint is already registered and `provider` restates it under its existing name; adding it again is rejected as a duplicate. */ + @JsonProperty("providerDisposition") ModelProviderConfigurationDisposition providerDisposition, + /** Whether `model` still needs to be registered. When `alreadyConfigured`, `selectionId` is already registered and the caller can select it without adding anything. */ + @JsonProperty("modelDisposition") ModelProviderConfigurationDisposition modelDisposition, + /** Non-fatal warnings carried over from the discovered model, such as capabilities the provider did not report. */ + @JsonProperty("warnings") List warnings +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionRpc.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionRpc.java index 1b0e1441b1..9c235ef2e3 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionRpc.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionRpc.java @@ -29,6 +29,8 @@ public final class SessionRpc { private final RpcCaller caller; private final String sessionId; + /** API methods for the {@code providers} namespace. */ + public final SessionProvidersApi providers; /** API methods for the {@code sandbox} namespace. */ public final SessionSandboxApi sandbox; /** API methods for the {@code gitHubAuth} namespace. */ @@ -129,6 +131,7 @@ public final class SessionRpc { public SessionRpc(RpcCaller caller, String sessionId) { this.caller = caller; this.sessionId = sessionId; + this.providers = new SessionProvidersApi(caller, sessionId); this.sandbox = new SessionSandboxApi(caller, sessionId); this.gitHubAuth = new SessionGitHubAuthApi(caller, sessionId); this.accounts = new SessionAccountsApi(caller, sessionId); diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionUiApi.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionUiApi.java index b3e16d7c27..7feccbd56a 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionUiApi.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionUiApi.java @@ -78,6 +78,22 @@ public CompletableFuture handlePendingE return caller.invoke("session.ui.handlePendingElicitation", _p, SessionUiHandlePendingElicitationResult.class); } + /** + * Pending elicitation request ID and the user's response (accept/decline/cancel + form values). + *

+ * Note: the {@code sessionId} field in the params record is overridden + * by the session-scoped wrapper; any value provided is ignored. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ + @CopilotExperimental + CompletableFuture handleHumanAskUser(SessionUiHandleHumanAskUserParams params) { + com.fasterxml.jackson.databind.node.ObjectNode _p = MAPPER.valueToTree(params); + _p.put("sessionId", this.sessionId); + return caller.invoke("session.ui.handleHumanAskUser", _p, UIElicitationResult.class); + } + /** * Request ID of a pending `user_input.requested` event and the user's response. *

@@ -94,6 +110,22 @@ public CompletableFuture handlePendingUse return caller.invoke("session.ui.handlePendingUserInput", _p, SessionUiHandlePendingUserInputResult.class); } + /** + * Request ID of a pending `user_input.requested` event and the user's response. + *

+ * Note: the {@code sessionId} field in the params record is overridden + * by the session-scoped wrapper; any value provided is ignored. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ + @CopilotExperimental + CompletableFuture handleHumanUserInput(SessionUiHandleHumanUserInputParams params) { + com.fasterxml.jackson.databind.node.ObjectNode _p = MAPPER.valueToTree(params); + _p.put("sessionId", this.sessionId); + return caller.invoke("session.ui.handleHumanUserInput", _p, UIHandlePendingResult.class); + } + /** * Request ID of a pending `sampling.requested` event and an optional sampling result payload (omit to reject). *

@@ -158,6 +190,22 @@ public CompletableFuture handlePending return caller.invoke("session.ui.handlePendingExitPlanMode", _p, SessionUiHandlePendingExitPlanModeResult.class); } + /** + * Request ID of a pending `exit_plan_mode.requested` event and the user's response. + *

+ * Note: the {@code sessionId} field in the params record is overridden + * by the session-scoped wrapper; any value provided is ignored. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ + @CopilotExperimental + CompletableFuture handleHumanExitPlanMode(SessionUiHandleHumanExitPlanModeParams params) { + com.fasterxml.jackson.databind.node.ObjectNode _p = MAPPER.valueToTree(params); + _p.put("sessionId", this.sessionId); + return caller.invoke("session.ui.handleHumanExitPlanMode", _p, UIHandlePendingResult.class); + } + /** * Identifies the target session. * diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionUiHandleHumanAskUserParams.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionUiHandleHumanAskUserParams.java new file mode 100644 index 0000000000..de9e0419e7 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionUiHandleHumanAskUserParams.java @@ -0,0 +1,34 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.github.copilot.CopilotExperimental; +import javax.annotation.processing.Generated; + +/** + * Pending elicitation request ID and the user's response (accept/decline/cancel + form values). + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ +@CopilotExperimental +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +record SessionUiHandleHumanAskUserParams( + /** Target session identifier */ + @JsonProperty("sessionId") String sessionId, + /** The unique request ID from the elicitation.requested event */ + @JsonProperty("requestId") String requestId, + /** The elicitation response (accept with form values, decline, or cancel) */ + @JsonProperty("result") UIElicitationResponse result +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionUiHandleHumanExitPlanModeParams.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionUiHandleHumanExitPlanModeParams.java new file mode 100644 index 0000000000..aab4e6d7cb --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionUiHandleHumanExitPlanModeParams.java @@ -0,0 +1,34 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.github.copilot.CopilotExperimental; +import javax.annotation.processing.Generated; + +/** + * Request ID of a pending `exit_plan_mode.requested` event and the user's response. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ +@CopilotExperimental +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +record SessionUiHandleHumanExitPlanModeParams( + /** Target session identifier */ + @JsonProperty("sessionId") String sessionId, + /** The unique request ID from the exit_plan_mode.requested event */ + @JsonProperty("requestId") String requestId, + /** User response for a pending exit-plan-mode request, with approval state, selected action, auto-approve flag, and feedback. */ + @JsonProperty("response") UIExitPlanModeResponse response +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionUiHandleHumanUserInputParams.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionUiHandleHumanUserInputParams.java new file mode 100644 index 0000000000..45629b962d --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionUiHandleHumanUserInputParams.java @@ -0,0 +1,34 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.github.copilot.CopilotExperimental; +import javax.annotation.processing.Generated; + +/** + * Request ID of a pending `user_input.requested` event and the user's response. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ +@CopilotExperimental +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +record SessionUiHandleHumanUserInputParams( + /** Target session identifier */ + @JsonProperty("sessionId") String sessionId, + /** The unique request ID from the user_input.requested event */ + @JsonProperty("requestId") String requestId, + /** User response for a pending user-input request, with answer text and whether it was typed freeform. */ + @JsonProperty("response") UIUserInputResponse response +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/UIElicitationResult.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/UIElicitationResult.java new file mode 100644 index 0000000000..7b5dbb2d00 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/UIElicitationResult.java @@ -0,0 +1,27 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Indicates whether the elicitation response was accepted; false if it was already resolved by another client. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record UIElicitationResult( + /** Whether the response was accepted. False if the request was already resolved by another client. */ + @JsonProperty("success") Boolean success +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/UIHandlePendingResult.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/UIHandlePendingResult.java new file mode 100644 index 0000000000..a71e3af6b3 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/UIHandlePendingResult.java @@ -0,0 +1,27 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Indicates whether the pending UI request was resolved by this call. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record UIHandlePendingResult( + /** True if the request was still pending and was resolved by this call. False if the request ID was unknown, already resolved by another client (e.g. GitHub), expired, or otherwise no longer pending. */ + @JsonProperty("success") Boolean success +) { +} diff --git a/java/sdk/src/main/java/com/github/copilot/CopilotSession.java b/java/sdk/src/main/java/com/github/copilot/CopilotSession.java index 5122d12f36..ddd46aa262 100644 --- a/java/sdk/src/main/java/com/github/copilot/CopilotSession.java +++ b/java/sdk/src/main/java/com/github/copilot/CopilotSession.java @@ -112,6 +112,8 @@ import com.github.copilot.rpc.SessionStartHookInput; import com.github.copilot.rpc.SessionUiApi; import com.github.copilot.rpc.SessionUiCapabilities; +import com.github.copilot.rpc.SubagentStartHookInput; +import com.github.copilot.rpc.SubagentStopHookInput; import com.github.copilot.rpc.ToolDefinition; import com.github.copilot.rpc.ToolResultObject; import com.github.copilot.rpc.TranscriptRecoveryReport; @@ -2293,6 +2295,26 @@ CompletableFuture handleHooksInvoke(String hookType, JsonNode input) { return stopResult.thenApply(output -> (Object) output); } break; + case "subagentStart" : + if (hooks.getOnSubagentStart() != null) { + SubagentStartHookInput startInput = MAPPER.treeToValue(input, SubagentStartHookInput.class); + var startResult = hooks.getOnSubagentStart().handle(startInput, invocation); + if (startResult == null) { + return CompletableFuture.completedFuture(null); + } + return startResult.thenApply(output -> (Object) output); + } + break; + case "subagentStop" : + if (hooks.getOnSubagentStop() != null) { + SubagentStopHookInput stopInput = MAPPER.treeToValue(input, SubagentStopHookInput.class); + var stopResult = hooks.getOnSubagentStop().handle(stopInput, invocation); + if (stopResult == null) { + return CompletableFuture.completedFuture(null); + } + return stopResult.thenApply(output -> (Object) output); + } + break; default : LOG.fine("Unhandled hook type: " + hookType); } diff --git a/java/sdk/src/main/java/com/github/copilot/rpc/SessionHooks.java b/java/sdk/src/main/java/com/github/copilot/rpc/SessionHooks.java index e476f888ed..30febc54ce 100644 --- a/java/sdk/src/main/java/com/github/copilot/rpc/SessionHooks.java +++ b/java/sdk/src/main/java/com/github/copilot/rpc/SessionHooks.java @@ -46,6 +46,8 @@ public class SessionHooks { private SessionStartHandler onSessionStart; private SessionEndHandler onSessionEnd; private AgentStopHandler onAgentStop; + private SubagentStartHandler onSubagentStart; + private SubagentStopHandler onSubagentStop; /** * Gets the pre-tool-use handler. @@ -254,6 +256,48 @@ public SessionHooks setOnAgentStop(AgentStopHandler onAgentStop) { return this; } + /** + * Gets the handler called before a subagent's first turn. + * + * @return the handler, or {@code null} if not set + */ + public SubagentStartHandler getOnSubagentStart() { + return onSubagentStart; + } + + /** + * Sets the handler called before a subagent's first turn. + * + * @param onSubagentStart + * the handler + * @return this instance for method chaining + */ + public SessionHooks setOnSubagentStart(SubagentStartHandler onSubagentStart) { + this.onSubagentStart = onSubagentStart; + return this; + } + + /** + * Gets the handler called after a subagent's turn. + * + * @return the handler, or {@code null} if not set + */ + public SubagentStopHandler getOnSubagentStop() { + return onSubagentStop; + } + + /** + * Sets the handler called after a subagent's turn. + * + * @param onSubagentStop + * the handler + * @return this instance for method chaining + */ + public SessionHooks setOnSubagentStop(SubagentStopHandler onSubagentStop) { + this.onSubagentStop = onSubagentStop; + return this; + } + /** * Returns whether any hooks are registered. * @@ -262,6 +306,6 @@ public SessionHooks setOnAgentStop(AgentStopHandler onAgentStop) { public boolean hasHooks() { return onPreToolUse != null || onPreMcpToolCall != null || onPostToolUse != null || onPostToolUseFailure != null || onUserPromptSubmitted != null || onUserPromptTransformed != null || onSessionStart != null - || onSessionEnd != null || onAgentStop != null; + || onSessionEnd != null || onAgentStop != null || onSubagentStart != null || onSubagentStop != null; } } diff --git a/java/sdk/src/main/java/com/github/copilot/rpc/SubagentStartHandler.java b/java/sdk/src/main/java/com/github/copilot/rpc/SubagentStartHandler.java new file mode 100644 index 0000000000..273da17aac --- /dev/null +++ b/java/sdk/src/main/java/com/github/copilot/rpc/SubagentStartHandler.java @@ -0,0 +1,25 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.rpc; + +import java.util.concurrent.CompletableFuture; + +/** + * Handler invoked before a subagent's first turn. + */ +@FunctionalInterface +public interface SubagentStartHandler { + + /** + * Handles a subagent-start hook invocation. + * + * @param input + * the hook input + * @param invocation + * context about the invocation + * @return context to prepend to the subagent's prompt, or {@code null} + */ + CompletableFuture handle(SubagentStartHookInput input, HookInvocation invocation); +} diff --git a/java/sdk/src/main/java/com/github/copilot/rpc/SubagentStartHookInput.java b/java/sdk/src/main/java/com/github/copilot/rpc/SubagentStartHookInput.java new file mode 100644 index 0000000000..0ab4d426b4 --- /dev/null +++ b/java/sdk/src/main/java/com/github/copilot/rpc/SubagentStartHookInput.java @@ -0,0 +1,34 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * Input received before a subagent's first turn. + * + * @param sessionId + * the parent session ID + * @param timestamp + * Unix timestamp in milliseconds + * @param cwd + * the parent session's working directory + * @param transcriptPath + * the parent session transcript path + * @param agentName + * the subagent definition name + * @param agentDisplayName + * the optional subagent display name + * @param agentDescription + * the optional subagent description + */ +@JsonIgnoreProperties(ignoreUnknown = true) +public record SubagentStartHookInput(@JsonProperty("sessionId") String sessionId, + @JsonProperty("timestamp") long timestamp, @JsonProperty("cwd") String cwd, + @JsonProperty("transcriptPath") String transcriptPath, @JsonProperty("agentName") String agentName, + @JsonProperty("agentDisplayName") String agentDisplayName, + @JsonProperty("agentDescription") String agentDescription) { +} diff --git a/java/sdk/src/main/java/com/github/copilot/rpc/SubagentStartHookOutput.java b/java/sdk/src/main/java/com/github/copilot/rpc/SubagentStartHookOutput.java new file mode 100644 index 0000000000..5a19ad594f --- /dev/null +++ b/java/sdk/src/main/java/com/github/copilot/rpc/SubagentStartHookOutput.java @@ -0,0 +1,19 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.rpc; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * Output for a subagent-start hook. + * + * @param additionalContext + * context prepended to the subagent's initial prompt, or + * {@code null} + */ +@JsonInclude(JsonInclude.Include.NON_NULL) +public record SubagentStartHookOutput(@JsonProperty("additionalContext") String additionalContext) { +} diff --git a/java/sdk/src/main/java/com/github/copilot/rpc/SubagentStopHandler.java b/java/sdk/src/main/java/com/github/copilot/rpc/SubagentStopHandler.java new file mode 100644 index 0000000000..c4c00fd622 --- /dev/null +++ b/java/sdk/src/main/java/com/github/copilot/rpc/SubagentStopHandler.java @@ -0,0 +1,26 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.rpc; + +import java.util.concurrent.CompletableFuture; + +/** + * Handler invoked after a subagent's turn. + */ +@FunctionalInterface +public interface SubagentStopHandler { + + /** + * Handles a subagent-stop hook invocation. + * + * @param input + * the hook input + * @param invocation + * context about the invocation + * @return a block decision or replacement response, or {@code null} to allow + * the stop + */ + CompletableFuture handle(SubagentStopHookInput input, HookInvocation invocation); +} diff --git a/java/sdk/src/main/java/com/github/copilot/rpc/SubagentStopHookInput.java b/java/sdk/src/main/java/com/github/copilot/rpc/SubagentStopHookInput.java new file mode 100644 index 0000000000..299238e034 --- /dev/null +++ b/java/sdk/src/main/java/com/github/copilot/rpc/SubagentStopHookInput.java @@ -0,0 +1,44 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * Input received after a subagent's turn. + * + * @param sessionId + * the parent session ID + * @param timestamp + * Unix timestamp in milliseconds + * @param cwd + * the parent session's working directory + * @param transcriptPath + * the parent session transcript path + * @param agentName + * the subagent definition name + * @param agentType + * the subagent type + * @param agentId + * the subagent ID, if available + * @param agentDisplayName + * the optional subagent display name + * @param agentDescription + * the optional subagent description + * @param stopReason + * the reason the subagent stopped + * @param response + * the subagent's last assistant response + */ +@JsonIgnoreProperties(ignoreUnknown = true) +public record SubagentStopHookInput(@JsonProperty("sessionId") String sessionId, + @JsonProperty("timestamp") long timestamp, @JsonProperty("cwd") String cwd, + @JsonProperty("transcriptPath") String transcriptPath, @JsonProperty("agentName") String agentName, + @JsonProperty("agentType") String agentType, @JsonProperty("agentId") String agentId, + @JsonProperty("agentDisplayName") String agentDisplayName, + @JsonProperty("agentDescription") String agentDescription, @JsonProperty("stopReason") String stopReason, + @JsonProperty("response") String response) { +} diff --git a/java/sdk/src/main/java/com/github/copilot/rpc/SubagentStopHookOutput.java b/java/sdk/src/main/java/com/github/copilot/rpc/SubagentStopHookOutput.java new file mode 100644 index 0000000000..364d7ea88e --- /dev/null +++ b/java/sdk/src/main/java/com/github/copilot/rpc/SubagentStopHookOutput.java @@ -0,0 +1,26 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.rpc; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * Output for a subagent-stop hook. + * + * @param decision + * {@code "block"} to run another subagent turn, or {@code "allow"} + * (or {@code null}) to allow the stop; other values fail the + * subagent + * @param reason + * the nonempty follow-up instruction required when blocking; invalid + * without {@code decision = "block"} + * @param modifiedResponse + * replacement final response when the stop is allowed + */ +@JsonInclude(JsonInclude.Include.NON_NULL) +public record SubagentStopHookOutput(@JsonProperty("decision") String decision, @JsonProperty("reason") String reason, + @JsonProperty("modifiedResponse") String modifiedResponse) { +} diff --git a/java/sdk/src/main/java/com/github/copilot/rpc/SystemMessageSections.java b/java/sdk/src/main/java/com/github/copilot/rpc/SystemMessageSections.java index ca410e497e..d575622f00 100644 --- a/java/sdk/src/main/java/com/github/copilot/rpc/SystemMessageSections.java +++ b/java/sdk/src/main/java/com/github/copilot/rpc/SystemMessageSections.java @@ -61,9 +61,9 @@ public abstract sealed class SystemMessageSections permits SystemPromptSections public static final String CUSTOM_INSTRUCTIONS = "custom_instructions"; /** - * Runtime-provided context and instructions (e.g. system notifications, - * memories, workspace context, mode-specific instructions, content-exclusion - * policy). + * Runtime-provided system-prompt context and instructions, such as system + * notifications, memories, workspace context, and content-exclusion policy. + * Mode-specific instructions can travel in transition messages instead. * * @since 1.3.0 */ @@ -71,7 +71,8 @@ public abstract sealed class SystemMessageSections permits SystemPromptSections /** * End-of-prompt instructions: parallel tool calling, persistence, task - * completion. + * completion, and configured subagent-model guidance when the task tool is + * available. */ public static final String LAST_INSTRUCTIONS = "last_instructions"; diff --git a/java/sdk/src/test/java/com/github/copilot/SessionEventHandlingTest.java b/java/sdk/src/test/java/com/github/copilot/SessionEventHandlingTest.java index d8bc68906b..46b71d7d3f 100644 --- a/java/sdk/src/test/java/com/github/copilot/SessionEventHandlingTest.java +++ b/java/sdk/src/test/java/com/github/copilot/SessionEventHandlingTest.java @@ -387,7 +387,7 @@ void testHandlerReceivesCorrectEventData() { SessionStartEvent startEvent = createSessionStartEvent(); startEvent.setData(new SessionStartEvent.SessionStartEventData("my-session-123", null, null, null, null, null, - null, null, null, null, null, null, null, null, null, null, null)); + null, null, null, null, null, null, null, null, null, null, null, null)); dispatchEvent(startEvent); AssistantMessageEvent msgEvent = createAssistantMessageEvent("Test content"); @@ -1064,7 +1064,7 @@ private SessionStartEvent createSessionStartEvent() { private SessionStartEvent createSessionStartEvent(String sessionId) { var event = new SessionStartEvent(); var data = new SessionStartEvent.SessionStartEventData(sessionId, null, null, null, null, null, null, null, - null, null, null, null, null, null, null, null, null); + null, null, null, null, null, null, null, null, null, null); event.setData(data); return event; } diff --git a/java/sdk/src/test/java/com/github/copilot/SessionHandlerTest.java b/java/sdk/src/test/java/com/github/copilot/SessionHandlerTest.java index 345fdccff9..001489e41f 100644 --- a/java/sdk/src/test/java/com/github/copilot/SessionHandlerTest.java +++ b/java/sdk/src/test/java/com/github/copilot/SessionHandlerTest.java @@ -6,10 +6,15 @@ import static org.junit.jupiter.api.Assertions.*; +import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutionException; +import java.util.logging.Handler; +import java.util.logging.Level; +import java.util.logging.LogRecord; +import java.util.logging.Logger; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -22,6 +27,8 @@ import com.github.copilot.rpc.SessionEndHookOutput; import com.github.copilot.rpc.SessionHooks; import com.github.copilot.rpc.SessionStartHookOutput; +import com.github.copilot.rpc.SubagentStartHookOutput; +import com.github.copilot.rpc.SubagentStopHookOutput; import com.github.copilot.rpc.ToolDefinition; import com.github.copilot.rpc.UserInputRequest; import com.github.copilot.rpc.UserInputResponse; @@ -310,6 +317,81 @@ void testHandleHooksInvokeAgentStop() throws Exception { assertEquals("finish the remaining work", output.getReason()); } + @Test + void testHandleHooksInvokeSubagentStart() throws Exception { + var hooks = new SessionHooks().setOnSubagentStart((hookInput, invocation) -> { + assertEquals("handler-test-session", invocation.getSessionId()); + assertEquals("parent-session", hookInput.sessionId()); + assertEquals(1735689600000L, hookInput.timestamp()); + assertEquals("/tmp", hookInput.cwd()); + assertEquals("/tmp/transcript.jsonl", hookInput.transcriptPath()); + assertEquals("explore", hookInput.agentName()); + assertEquals("Explore Agent", hookInput.agentDisplayName()); + assertEquals("Read code", hookInput.agentDescription()); + return CompletableFuture.completedFuture(new SubagentStartHookOutput("Follow the file")); + }); + assertTrue(hooks.hasHooks()); + session.registerHooks(hooks); + + JsonNode input = MAPPER.valueToTree(Map.of("sessionId", "parent-session", "timestamp", 1735689600000L, "cwd", + "/tmp", "transcriptPath", "/tmp/transcript.jsonl", "agentName", "explore", "agentDisplayName", + "Explore Agent", "agentDescription", "Read code")); + + var output = assertInstanceOf(SubagentStartHookOutput.class, + session.handleHooksInvoke("subagentStart", input).get()); + assertEquals("Follow the file", MAPPER.valueToTree(output).get("additionalContext").asText()); + } + + @Test + void testHandleHooksInvokeSubagentStop() throws Exception { + var hooks = new SessionHooks().setOnSubagentStop((hookInput, invocation) -> { + assertEquals("handler-test-session", invocation.getSessionId()); + assertEquals("parent-session", hookInput.sessionId()); + assertEquals(1735689600001L, hookInput.timestamp()); + assertEquals("/tmp", hookInput.cwd()); + assertEquals("/tmp/transcript.jsonl", hookInput.transcriptPath()); + assertEquals("explore", hookInput.agentName()); + assertEquals("explore", hookInput.agentType()); + assertEquals("read-file", hookInput.agentId()); + assertEquals("Explore Agent", hookInput.agentDisplayName()); + assertEquals("Read code", hookInput.agentDescription()); + assertEquals("end_turn", hookInput.stopReason()); + assertEquals("original answer", hookInput.response()); + return CompletableFuture.completedFuture(new SubagentStopHookOutput(null, null, "rewritten answer")); + }); + assertTrue(hooks.hasHooks()); + session.registerHooks(hooks); + + JsonNode input = MAPPER.valueToTree(Map.ofEntries(Map.entry("sessionId", "parent-session"), + Map.entry("timestamp", 1735689600001L), Map.entry("cwd", "/tmp"), + Map.entry("transcriptPath", "/tmp/transcript.jsonl"), Map.entry("agentName", "explore"), + Map.entry("agentType", "explore"), Map.entry("agentId", "read-file"), + Map.entry("agentDisplayName", "Explore Agent"), Map.entry("agentDescription", "Read code"), + Map.entry("stopReason", "end_turn"), Map.entry("response", "original answer"))); + + var output = assertInstanceOf(SubagentStopHookOutput.class, + session.handleHooksInvoke("subagentStop", input).get()); + JsonNode result = MAPPER.valueToTree(output); + assertEquals("rewritten answer", result.get("modifiedResponse").asText()); + assertFalse(result.has("decision")); + assertFalse(result.has("reason")); + } + + @Test + void testHandleHooksInvokeSubagentStopBlock() throws Exception { + session.registerHooks(new SessionHooks().setOnSubagentStop((input, invocation) -> CompletableFuture + .completedFuture(new SubagentStopHookOutput("block", "Keep researching", null)))); + + JsonNode result = MAPPER + .valueToTree(session + .handleHooksInvoke("subagentStop", + MAPPER.valueToTree(Map.of("sessionId", "parent-session", "agentName", "explore"))) + .get()); + assertEquals("block", result.get("decision").asText()); + assertEquals("Keep researching", result.get("reason").asText()); + assertFalse(result.has("modifiedResponse")); + } + // ===== handleHooksInvoke: sessionId deserialization on hook inputs ===== @Test @@ -371,6 +453,44 @@ void testHandleHooksInvokeUnhandledHookType() throws Exception { assertNull(result); } + @Test + void testHandleHooksInvokeSubagentLifecycleDoesNotLogAsUnhandled() throws Exception { + session.registerHooks(new SessionHooks()); + Logger logger = Logger.getLogger(CopilotSession.class.getName()); + Level previousLevel = logger.getLevel(); + List unhandled = new ArrayList<>(); + Handler handler = new Handler() { + @Override + public void publish(LogRecord record) { + if (record.getMessage().startsWith("Unhandled hook type: ")) { + unhandled.add(record.getMessage()); + } + } + + @Override + public void flush() { + } + + @Override + public void close() { + } + }; + handler.setLevel(Level.FINE); + logger.addHandler(handler); + logger.setLevel(Level.FINE); + try { + JsonNode input = MAPPER.valueToTree(Map.of("sessionId", "handler-test-session", "agentName", "task")); + assertNull(session.handleHooksInvoke("subagentStart", input).get()); + assertNull(session.handleHooksInvoke("subagentStop", input).get()); + assertNull(session.handleHooksInvoke("unknownHookType", input).get()); + + assertEquals(List.of("Unhandled hook type: unknownHookType"), unhandled); + } finally { + logger.removeHandler(handler); + logger.setLevel(previousLevel); + } + } + // ===== handleHooksInvoke: handler throws ===== @Test diff --git a/java/sdk/src/test/java/com/github/copilot/StringSchemaToolsIT.java b/java/sdk/src/test/java/com/github/copilot/StringSchemaToolsIT.java new file mode 100644 index 0000000000..3549b6b3d6 --- /dev/null +++ b/java/sdk/src/test/java/com/github/copilot/StringSchemaToolsIT.java @@ -0,0 +1,49 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot; + +import static org.junit.jupiter.api.Assertions.*; + +import java.nio.file.Files; +import java.util.List; +import java.util.Map; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeUnit; + +import org.junit.jupiter.api.Test; + +import com.github.copilot.rpc.MessageOptions; +import com.github.copilot.rpc.PermissionHandler; +import com.github.copilot.rpc.SessionConfig; +import com.github.copilot.rpc.ToolDefinition; + +class StringSchemaToolsIT { + @Test + void stringSchemaApplyPatchOverrideBindsPatchInput() throws Exception { + try (E2ETestContext ctx = E2ETestContext.create()) { + ctx.configureForTest("tools", "string_schema_apply_patch_override_binds_patch_input"); + var receivedPatch = new CompletableFuture(); + var tool = ToolDefinition.create("apply_patch", "Apply a patch", Map.of("type", "string"), invocation -> { + receivedPatch.complete(invocation.getArgumentsAs(String.class)); + return CompletableFuture.completedFuture("HOST_PATCH_HANDLED"); + }).overridesBuiltInTool(true); + try (CopilotClient client = ctx.createClient(); + CopilotSession session = client.createSession(new SessionConfig() + .setOnPermissionRequest(PermissionHandler.APPROVE_ALL).setTools(List.of(tool))) + .get(30, TimeUnit.SECONDS)) { + var response = session + .sendAndWait(new MessageOptions().setPrompt("Use apply_patch to apply the supplied patch.")) + .get(60, TimeUnit.SECONDS); + + assertTrue(receivedPatch.isDone(), "Typed string handler was not invoked"); + assertEquals("*** Begin Patch\n*** Add File: override-marker.txt\n+from-native\n*** End Patch", + receivedPatch.get(10, TimeUnit.SECONDS)); + assertNotNull(response); + assertEquals("Host override completed.", response.getData().content()); + assertFalse(Files.exists(ctx.getWorkDir().resolve("override-marker.txt"))); + } + } + } +} diff --git a/java/sdk/src/test/java/com/github/copilot/SubagentHooksE2ETest.java b/java/sdk/src/test/java/com/github/copilot/SubagentHooksE2ETest.java index 009773bb54..536a33cb95 100644 --- a/java/sdk/src/test/java/com/github/copilot/SubagentHooksE2ETest.java +++ b/java/sdk/src/test/java/com/github/copilot/SubagentHooksE2ETest.java @@ -4,16 +4,18 @@ package com.github.copilot; -import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertTrue; import java.io.InputStream; import java.net.http.HttpRequest; import java.net.http.HttpResponse; import java.nio.file.Files; +import java.nio.file.Path; import java.util.HashMap; import java.util.List; import java.util.UUID; @@ -23,6 +25,8 @@ import org.junit.jupiter.api.Test; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; import com.github.copilot.generated.AssistantMessageEvent; import com.github.copilot.rpc.CopilotClientOptions; import com.github.copilot.rpc.MessageOptions; @@ -31,20 +35,33 @@ import com.github.copilot.rpc.PreToolUseHookOutput; import com.github.copilot.rpc.SessionConfig; import com.github.copilot.rpc.SessionHooks; +import com.github.copilot.rpc.SubagentStartHookInput; +import com.github.copilot.rpc.SubagentStartHookOutput; +import com.github.copilot.rpc.SubagentStopHookInput; +import com.github.copilot.rpc.SubagentStopHookOutput; public class SubagentHooksE2ETest { - private static final String SNAPSHOT_NAME = "should_invoke_pretooluse_and_posttooluse_hooks_for_sub_agent_tool_calls"; + private static final String SNAPSHOT_NAME = "should_apply_subagent_lifecycle_hook_outputs"; + private static final String CHILD_CONTEXT = "Subagent start hook verified: read the requested file."; + private static final String STOP_RESPONSE_PREFIX = "Subagent stop hook verified: "; @Test - void shouldInvokePreToolUseAndPostToolUseHooksForSubAgentToolCalls() throws Exception { + void shouldApplySubagentLifecycleHookOutputs() throws Exception { try (E2ETestContext ctx = E2ETestContext.create()) { ctx.configureForTest("subagent_hooks", SNAPSHOT_NAME); ConcurrentLinkedQueue hookLog = new ConcurrentLinkedQueue<>(); + ConcurrentLinkedQueue subagentStarts = new ConcurrentLinkedQueue<>(); + ConcurrentLinkedQueue subagentStops = new ConcurrentLinkedQueue<>(); + ConcurrentLinkedQueue startInvocationIds = new ConcurrentLinkedQueue<>(); + ConcurrentLinkedQueue stopInvocationIds = new ConcurrentLinkedQueue<>(); + CompletableFuture subagentStopped = new CompletableFuture<>(); RecordingForwardingRequestHandler requestHandler = new RecordingForwardingRequestHandler(); - String waitingText = "I've launched an explore agent to read subagent-test.txt. Waiting for it to complete..."; - String finalText = "The explore agent successfully read the file. The contents of **subagent-test.txt** are:\n\n```\nHello from subagent test!\n```"; + String waitingText = "I've launched an explore agent to read subagent-test.txt. " + + "Waiting for it to complete..."; + String finalText = "The explore agent successfully read the file. " + + "The contents of **subagent-test.txt** are:\n\n```\nHello from subagent test!\n```"; String parentSessionId = UUID.randomUUID().toString(); CompletableFuture parentWaiting = new CompletableFuture<>(); HashMap env = new HashMap<>(ctx.getEnvironment()); @@ -65,6 +82,16 @@ void shouldInvokePreToolUseAndPostToolUseHooksForSubAgentToolCalls() throws Exce return parentWaiting.thenApply(ignored -> null); } return CompletableFuture.completedFuture((PostToolUseHookOutput) null); + }).setOnSubagentStart((input, invocation) -> { + subagentStarts.add(input); + startInvocationIds.add(invocation.getSessionId()); + return CompletableFuture.completedFuture(new SubagentStartHookOutput(CHILD_CONTEXT)); + }).setOnSubagentStop((input, invocation) -> { + subagentStops.add(input); + stopInvocationIds.add(invocation.getSessionId()); + subagentStopped.complete(input); + return CompletableFuture.completedFuture( + new SubagentStopHookOutput(null, null, STOP_RESPONSE_PREFIX + input.response())); }))).get(); try (var subscription = session.on(AssistantMessageEvent.class, message -> { if ((message.getAgentId() == null || message.getAgentId().isEmpty()) @@ -72,43 +99,112 @@ void shouldInvokePreToolUseAndPostToolUseHooksForSubAgentToolCalls() throws Exce parentWaiting.complete(null); } })) { - Files.writeString(ctx.getWorkDir().resolve("subagent-test.txt"), "Hello from subagent test!"); - var response = session.sendAndWait(new MessageOptions() - .setPrompt("Use the task tool to spawn an explore agent that reads the file " - + "subagent-test.txt in the current directory and reports its contents. " - + "You must use the task tool.")) - .get(120, TimeUnit.SECONDS); - assertNotNull(response); - assertTrue(response.getAgentId() == null || response.getAgentId().isEmpty()); - assertEquals(finalText, response.getData().content()); - var replies = session.getMessages().get().stream() - .filter(event -> event instanceof AssistantMessageEvent - && (event.getAgentId() == null || event.getAgentId().isEmpty())) - .map(event -> ((AssistantMessageEvent) event).getData().content()) - .filter(content -> waitingText.equals(content) || finalText.equals(content)).toList(); - assertEquals(List.of(waitingText, finalText), replies, - "Durable history must contain the waiting reply before the final reply"); - - HookEntry taskPre = hookLog.stream() - .filter(h -> h.kind().equals("pre") && h.toolName().equals("task")).findFirst() - .orElse(null); - assertNotNull(taskPre, "preToolUse should fire for the parent's 'task' tool call"); - - List viewPre = hookLog.stream() - .filter(h -> h.kind().equals("pre") && h.toolName().equals("view")).toList(); - List viewPost = hookLog.stream() - .filter(h -> h.kind().equals("post") && h.toolName().equals("view")).toList(); - assertFalse(viewPre.isEmpty(), "preToolUse should fire for the sub-agent's 'view' tool call"); - assertFalse(viewPost.isEmpty(), "postToolUse should fire for the sub-agent's 'view' tool call"); - assertNotEquals(taskPre.sessionId(), viewPre.get(0).sessionId(), - "Sub-agent tool hooks should have a different sessionId than parent tool hooks"); - assertSubagentRequestMetadata(requestHandler.inferenceRequests()); - } finally { - parentWaiting.complete(null); - session.close(); + try { + Files.writeString(ctx.getWorkDir().resolve("subagent-test.txt"), "Hello from subagent test!"); + var response = session.sendAndWait(new MessageOptions() + .setPrompt("Use the task tool to spawn an explore agent that reads the file " + + "subagent-test.txt in the current directory and reports its contents. " + + "You must use the task tool.")) + .get(120, TimeUnit.SECONDS); + assertNotNull(response); + assertTrue(response.getAgentId() == null || response.getAgentId().isEmpty()); + assertEquals(finalText, response.getData().content()); + var replies = session.getMessages().get().stream() + .filter(event -> event instanceof AssistantMessageEvent + && (event.getAgentId() == null || event.getAgentId().isEmpty())) + .map(event -> ((AssistantMessageEvent) event).getData().content()) + .filter(content -> waitingText.equals(content) || finalText.equals(content)).toList(); + assertEquals(List.of(waitingText, finalText), replies, + "Durable history must contain the waiting reply before the final reply"); + subagentStopped.get(120, TimeUnit.SECONDS); + + HookEntry taskPre = hookLog.stream() + .filter(h -> h.kind().equals("pre") && h.toolName().equals("task")).findFirst() + .orElse(null); + assertNotNull(taskPre, "preToolUse should fire for the parent's 'task' tool call"); + + List viewPre = hookLog.stream() + .filter(h -> h.kind().equals("pre") && h.toolName().equals("view")).toList(); + List viewPost = hookLog.stream() + .filter(h -> h.kind().equals("post") && h.toolName().equals("view")).toList(); + assertFalse(viewPre.isEmpty(), "preToolUse should fire for the sub-agent's 'view' tool call"); + assertFalse(viewPost.isEmpty(), "postToolUse should fire for the sub-agent's 'view' tool call"); + assertNotEquals(taskPre.sessionId(), viewPre.get(0).sessionId(), + "Sub-agent tool hooks should have a different sessionId than parent tool hooks"); + assertSubagentRequestMetadata(requestHandler.inferenceRequests()); + ObjectMapper mapper = new ObjectMapper(); + boolean childContextObserved = false; + for (RequestRecord request : requestHandler.inferenceRequests()) { + if (request.parentAgentId() == null || request.parentAgentId().isEmpty()) { + continue; + } + for (JsonNode message : mapper.readTree(request.body()).path("messages")) { + if (hasChildContext(message)) { + childContextObserved = true; + } + } + } + assertTrue(childContextObserved, + "start hook context should be prepended to the child inference prompt"); + assertTrue( + requestHandler.inferenceRequests().stream() + .anyMatch(r -> (r.parentAgentId() == null || r.parentAgentId().isEmpty()) + && r.body().contains(STOP_RESPONSE_PREFIX)), + "rewritten stop response should reach a parent inference request"); + + assertEquals(1, subagentStarts.size(), "one start hook per launched subagent"); + assertEquals(List.of(session.getSessionId()), List.copyOf(startInvocationIds)); + SubagentStartHookInput start = subagentStarts.element(); + assertEquals(session.getSessionId(), start.sessionId()); + assertTrue(start.timestamp() > 0); + assertEquals(ctx.getWorkDir().toAbsolutePath().normalize(), + Path.of(start.cwd()).toAbsolutePath().normalize()); + assertNotNull(start.transcriptPath()); + assertEquals("explore", start.agentName()); + assertNull(start.agentDisplayName()); + assertNull(start.agentDescription()); + + assertEquals(1, subagentStops.size(), "one stop hook per completed subagent"); + assertEquals(List.of(session.getSessionId()), List.copyOf(stopInvocationIds)); + SubagentStopHookInput stop = subagentStops.element(); + assertEquals(start.sessionId(), stop.sessionId()); + assertTrue(stop.timestamp() >= start.timestamp()); + assertEquals(start.cwd(), stop.cwd()); + assertEquals(start.transcriptPath(), stop.transcriptPath()); + assertEquals(start.agentName(), stop.agentName()); + assertEquals(start.agentDisplayName(), stop.agentDisplayName()); + assertEquals(start.agentDescription(), stop.agentDescription()); + assertEquals("explore", stop.agentType()); + assertNotNull(stop.agentId()); + assertFalse(stop.agentId().isBlank()); + assertEquals("end_turn", stop.stopReason()); + assertTrue(stop.response().contains("Hello from subagent test!")); + } finally { + parentWaiting.complete(null); + session.close(); + } + } + } + } + } + + private static boolean hasChildContext(JsonNode message) { + if (!"user".equals(message.path("role").asText())) { + return false; + } + String expected = CHILD_CONTEXT + "\n\nRead the file \"subagent-test.txt\""; + JsonNode content = message.path("content"); + if (content.isTextual()) { + return content.asText().contains(expected); + } + if (content.isArray()) { + for (JsonNode part : content) { + if ("text".equals(part.path("type").asText()) && part.path("text").asText().contains(expected)) { + return true; } } } + return false; } private static void assertSubagentRequestMetadata(List records) { @@ -132,7 +228,8 @@ private static boolean isInferenceUrl(String url) { private record HookEntry(String kind, String toolName, String sessionId) { } - private record RequestRecord(String url, String agentId, String parentAgentId, String interactionType) { + private record RequestRecord(String url, String agentId, String parentAgentId, String interactionType, + String body) { } private static final class RecordingForwardingRequestHandler extends CopilotRequestHandler { @@ -146,7 +243,10 @@ List inferenceRequests() { protected HttpResponse sendRequest(HttpRequest request, CopilotRequestContext ctx) throws Exception { records.add(new RequestRecord(request.uri().toString(), ctx.agentId(), ctx.parentAgentId(), - ctx.interactionType())); + ctx.interactionType(), + isInferenceUrl(request.uri().toString()) + ? CopilotRequestTestSupport.requestBodyText(request) + : "")); return super.sendRequest(request, ctx); } } diff --git a/java/sdk/src/test/java/com/github/copilot/generated/GeneratedTypesJacksonRoundTripTest.java b/java/sdk/src/test/java/com/github/copilot/generated/GeneratedTypesJacksonRoundTripTest.java index 1b9b99bb57..f1d40ba04d 100644 --- a/java/sdk/src/test/java/com/github/copilot/generated/GeneratedTypesJacksonRoundTripTest.java +++ b/java/sdk/src/test/java/com/github/copilot/generated/GeneratedTypesJacksonRoundTripTest.java @@ -16,6 +16,7 @@ import java.util.List; import org.junit.jupiter.api.DynamicTest; +import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestFactory; import com.fasterxml.jackson.databind.DeserializationFeature; @@ -53,6 +54,21 @@ private static ObjectMapper createMapper() { return mapper; } + @Test + void futureFileEditKindDoesNotRejectToolCompletion() throws IOException { + var event = MAPPER.readValue( + "{\"type\":\"tool.execution_complete\",\"data\":{\"toolCallId\":\"call-1\",\"success\":true," + + "\"fileEdits\":[{\"path\":\"/future.txt\",\"kind\":\"append\"}]}}", + ToolExecutionCompleteEvent.class); + + assertEquals("call-1", event.getData().toolCallId()); + assertEquals("/future.txt", event.getData().fileEdits().get(0).path()); + assertEquals("append", event.getData().fileEdits().get(0).kind().getValue()); + assertEquals("append", MAPPER.readTree(MAPPER.writeValueAsString(event)).path("data").path("fileEdits").get(0) + .path("kind").asText()); + assertEquals(ToolExecutionCompleteFileEditKind.CREATE, ToolExecutionCompleteFileEditKind.fromValue("create")); + } + @TestFactory Collection roundTripAllGeneratedRecords() { List tests = new ArrayList<>(); diff --git a/java/sdk/src/test/java/com/github/copilot/generated/rpc/CustomizationReloadOutcomeTest.java b/java/sdk/src/test/java/com/github/copilot/generated/rpc/CustomizationReloadOutcomeTest.java new file mode 100644 index 0000000000..e8c583024c --- /dev/null +++ b/java/sdk/src/test/java/com/github/copilot/generated/rpc/CustomizationReloadOutcomeTest.java @@ -0,0 +1,33 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.generated.rpc; + +import static org.junit.jupiter.api.Assertions.*; + +import org.junit.jupiter.api.Test; + +import com.fasterxml.jackson.databind.ObjectMapper; + +class CustomizationReloadOutcomeTest { + + private static final ObjectMapper MAPPER = new ObjectMapper(); + + @Test + void futureReloadValuesRoundTripWithoutChangingKnownValues() throws Exception { + var wire = MAPPER.readTree(""" + {"status":"newStatus","subsystem":"newSubsystem","detail":"new component"} + """); + var outcome = MAPPER.treeToValue(wire, CustomizationReloadOutcome.class); + + assertEquals("newStatus", outcome.status().getValue()); + assertEquals("newSubsystem", outcome.subsystem().getValue()); + assertEquals("new component", outcome.detail()); + assertEquals(wire, MAPPER.valueToTree(outcome)); + assertEquals(CustomizationReloadStatus.RELOADED, CustomizationReloadStatus.fromValue("reloaded")); + assertEquals(CustomizationReloadSubsystem.SKILLS, CustomizationReloadSubsystem.fromValue("skills")); + assertEquals("\"reloaded\"", MAPPER.writeValueAsString(CustomizationReloadStatus.RELOADED)); + assertEquals("\"skills\"", MAPPER.writeValueAsString(CustomizationReloadSubsystem.SKILLS)); + } +} diff --git a/java/sdk/src/test/java/com/github/copilot/generated/rpc/GeneratedRpcApiCoverageTest.java b/java/sdk/src/test/java/com/github/copilot/generated/rpc/GeneratedRpcApiCoverageTest.java index 4db5618aeb..3eb27a9178 100644 --- a/java/sdk/src/test/java/com/github/copilot/generated/rpc/GeneratedRpcApiCoverageTest.java +++ b/java/sdk/src/test/java/com/github/copilot/generated/rpc/GeneratedRpcApiCoverageTest.java @@ -74,10 +74,10 @@ static Stream newRpcDispatches() { dispatch("session.managedSettings.get", SessionManagedSettingsGetResult.class, """ {"sessionId":"bound-session"} """, (server, session) -> session.managedSettings.get()), - dispatch("session.instructions.reload", Void.class, """ + dispatch("session.instructions.reload", SessionInstructionsGetSourcesResult.class, """ {"sessionId":"bound-session"} """, (server, session) -> session.instructions.reload()), - dispatch("session.customizations.reload", SessionCustomizationsReloadResult.class, """ + dispatch("session.customizations.reload", CustomizationsReloadResult.class, """ {"sessionId":"bound-session"} """, (server, session) -> session.customizations.reload()), dispatch("session.plugins.install", SessionPluginsInstallResult.class, """ diff --git a/java/sdk/src/test/java/com/github/copilot/generated/rpc/GeneratedRpcRecordsCoverageTest.java b/java/sdk/src/test/java/com/github/copilot/generated/rpc/GeneratedRpcRecordsCoverageTest.java index 51fbaa865c..dace015cff 100644 --- a/java/sdk/src/test/java/com/github/copilot/generated/rpc/GeneratedRpcRecordsCoverageTest.java +++ b/java/sdk/src/test/java/com/github/copilot/generated/rpc/GeneratedRpcRecordsCoverageTest.java @@ -19,6 +19,7 @@ import org.junit.jupiter.params.provider.MethodSource; import com.fasterxml.jackson.databind.ObjectMapper; +import com.github.copilot.AllowCopilotExperimental; import com.github.copilot.TestUtil; /** @@ -222,6 +223,52 @@ void sessionFsSetProviderParams_conventions_enum() { assertThrows(IllegalArgumentException.class, () -> SessionFsSetProviderConventions.fromValue("unknown")); } + @Test + void sessionFsSetProviderCapabilities_preserves_legacy_constructor_and_binary_wire_field() throws Exception { + var mapper = new ObjectMapper(); + var legacy = new SessionFsSetProviderCapabilities(true); + var legacyJson = mapper.readTree(mapper.writeValueAsString(legacy)); + assertTrue(legacyJson.get("sqlite").asBoolean()); + assertFalse(legacyJson.has("binary")); + assertEquals(legacy, mapper.treeToValue(legacyJson, SessionFsSetProviderCapabilities.class)); + + var binary = new SessionFsSetProviderCapabilities(false, true); + var binaryJson = mapper.readTree(mapper.writeValueAsString(binary)); + assertFalse(binaryJson.get("sqlite").asBoolean()); + assertTrue(binaryJson.get("binary").asBoolean()); + assertEquals(binary, mapper.treeToValue(binaryJson, SessionFsSetProviderCapabilities.class)); + } + + @Test + @AllowCopilotExperimental + void sessionFsReadFileBytes_preserves_base64_wire_content() throws Exception { + var mapper = new ObjectMapper(); + var params = new SessionFsReadFileBytesParams("sess-15", "/image.png"); + var paramsJson = mapper.readTree(mapper.writeValueAsString(params)); + assertEquals("sess-15", paramsJson.get("sessionId").asText()); + assertEquals("/image.png", paramsJson.get("path").asText()); + assertEquals(params, mapper.treeToValue(paramsJson, SessionFsReadFileBytesParams.class)); + + var result = new SessionFsReadFileBytesResult("AAECAw==", null); + var resultJson = mapper.readTree(mapper.writeValueAsString(result)); + assertEquals("AAECAw==", resultJson.get("content").asText()); + assertFalse(resultJson.has("error")); + assertEquals(result, mapper.treeToValue(resultJson, SessionFsReadFileBytesResult.class)); + } + + @Test + @AllowCopilotExperimental + void sessionFsWriteFileBytes_preserves_base64_wire_content_and_mode() throws Exception { + var mapper = new ObjectMapper(); + var params = new SessionFsWriteFileBytesParams("sess-15", "/image.png", "AP/+AQ==", 384L); + var json = mapper.readTree(mapper.writeValueAsString(params)); + assertEquals("sess-15", json.get("sessionId").asText()); + assertEquals("/image.png", json.get("path").asText()); + assertEquals("AP/+AQ==", json.get("content").asText()); + assertEquals(384L, json.get("mode").asLong()); + assertEquals(params, mapper.treeToValue(json, SessionFsWriteFileBytesParams.class)); + } + @Test void sessionFsStatParams_record() { var params = new SessionFsStatParams("sess-20", "/etc/hosts"); @@ -850,8 +897,10 @@ void sessionMcpListResult_status_enum_all_values() { @Test void sessionModelGetCurrentResult_record() { - var result = new SessionModelGetCurrentResult("claude-sonnet-5", null, null, null, null, null); + var result = new SessionModelGetCurrentResult("claude-sonnet-5", "claude-sonnet-4.5", null, null, null, null, + null); assertEquals("claude-sonnet-5", result.modelId()); + assertEquals("claude-sonnet-4.5", result.planBaseModelId()); } @Test @@ -1015,7 +1064,7 @@ void mcpDiscoverResult_nested() { @Test void modelsListResult_nested() { - var supports = new ModelCapabilitiesSupports(true, null, false, null); + var supports = new ModelCapabilitiesSupports(false, true, null, false, null); var limits = new ModelCapabilitiesLimits(100000L, 8192L, 128000L, null); var capabilities = new ModelCapabilities(supports, limits); var policy = new ModelPolicy(ModelPolicyState.ENABLED, null); @@ -1028,6 +1077,7 @@ void modelsListResult_nested() { assertEquals(1, result.models().size()); assertEquals("gpt-5", result.models().get(0).id()); assertEquals("GPT-5", result.models().get(0).name()); + assertFalse(result.models().get(0).capabilities().supports().thinking()); assertTrue(result.models().get(0).capabilities().supports().vision()); assertFalse(result.models().get(0).capabilities().supports().reasoningEffort()); assertEquals(100000L, result.models().get(0).capabilities().limits().maxPromptTokens()); diff --git a/nodejs/README.md b/nodejs/README.md index 9ffc4f0327..1135d7ea08 100644 --- a/nodejs/README.md +++ b/nodejs/README.md @@ -270,6 +270,19 @@ new CopilotClient(options?: CopilotClientOptions) - `telemetry?: TelemetryConfig` - OpenTelemetry configuration for the runtime process. Providing this object enables telemetry — no separate flag needed. See [Telemetry](#telemetry) below. - `onGetTraceContext?: TraceContextProvider` - Advanced: callback for linking your application's own OpenTelemetry spans into the same distributed trace as the runtime's spans. Not needed for normal telemetry collection. See [Telemetry](#telemetry) below. - `sessionFs?: SessionFsConfig` - Custom session filesystem provider. + +For a custom provider to serve images to the `view` tool, declare +`sessionFs.capabilities.binary: true` and implement +`readFileBytes(path): Promise` and +`writeFileBytes(path, content: Uint8Array, mode?: number): Promise` on each session's provider. The +adapter transmits exact bytes as standard base64; text-only providers still +support text operations, but image reads fail rather than falling back to the +runtime machine's filesystem. A missing binary implementation is rejected +when a session is created. + +Binary reads and writes are limited to 50,330,880 raw bytes (approximately 48 MiB); +larger results return a filesystem error before encoding or decoding. + - `sessionIdleTimeoutSeconds?: number` - Server-wide idle timeout for sessions in seconds. Ignored when connecting via `RuntimeConnection.forUri`. - `enableRemoteSessions?: boolean` - Enable Mission Control remote session support. Ignored when connecting via `RuntimeConnection.forUri`. @@ -857,6 +870,22 @@ defineTool("edit_file", { }); ``` +An `apply_patch` override can declare a string schema. The model sees a required +`input` property, but the runtime restores the declared scalar shape before +dispatching to any SDK. Both a Zod-inferred Node handler and +`invocation.arguments` receive the patch text as a string: + +```ts +defineTool("apply_patch", { + parameters: z.string(), + overridesBuiltInTool: true, + handler: (patch) => patch.trim(), +}); +``` + +String-schema `apply_patch` overrides cannot contain JSON Schema references; +use an object schema if references are needed. + #### Skipping Permission Prompts Set `skipPermission: true` on a tool definition to allow it to execute without triggering a permission prompt: @@ -1006,6 +1035,8 @@ Available section IDs: `preamble`, `identity`, `tone`, `tool_efficiency`, `envir `identity` and `tool_instructions` are section _groups_ that target a collection of related sub-sections as a unit. Use `preamble` to target just the identity preamble without affecting its sibling sub-sections. +`last_instructions` includes configured subagent-model guidance when the `task` tool is available. Removing or replacing this section also removes that guidance; a transform callback receives the complete section, including the guidance, and its returned content is authoritative. Append, prepend, and preserve retain their usual section semantics. These overrides change prompt prose only, not configured subagent models, tool availability, or runtime dispatch policy. `runtime_instructions` is a separate section: removing it does not remove `last_instructions`. + Each section override supports five actions: - **`replace`** — Replace the section content entirely @@ -1014,6 +1045,8 @@ Each section override supports five actions: - **`prepend`** — Add content before the existing section - **`preserve`** — No-op that opts an individually-addressable section out of a group-level `remove` +An `action` can also be a callback that receives the current section content and returns the replacement content, synchronously or asynchronously. + Unknown section IDs are handled gracefully: content from `replace`/`append`/`prepend` overrides is appended to additional instructions, and `remove` overrides are silently ignored. #### Replace Mode @@ -1461,6 +1494,20 @@ const session = await client.createSession({ }; } }, + + // Called before a sub-agent's first turn (the input identifies the parent session) + onSubagentStart: (input, invocation) => { + console.log(`Starting ${input.agentDisplayName ?? input.agentName}`); + return { additionalContext: "Check the requested file before reporting back." }; + }, + + // Called when a sub-agent completes a turn + onSubagentStop: (input, invocation) => { + console.log(`${input.agentName} replied: ${input.response}`); + // Return { decision: "block", reason: "Continue checking the file." } + // to request another child turn instead. + return { modifiedResponse: `Reviewed: ${input.response}` }; + }, }, }); ``` @@ -1475,6 +1522,8 @@ const session = await client.createSession({ - `onSessionEnd` - Cleanup or logging when session ends. - `onErrorOccurred` - Handle errors with retry/skip/abort strategies. - `onAgentStop` - Observe natural top-level agent completion. Return `{ decision: "block", reason }` to request another turn; use `stopHookActive` to avoid repeated blocks. +- `onSubagentStart` - Observe a sub-agent before its first turn and prepend `additionalContext` to the child's prompt. +- `onSubagentStop` - Observe a sub-agent's final response. Return `{ decision: "block", reason }` to request another child turn, or `{ modifiedResponse }` to replace the response reported to the parent. ## Error Handling diff --git a/nodejs/docs/agent-author.md b/nodejs/docs/agent-author.md index 6b9366a7e6..73875cea14 100644 --- a/nodejs/docs/agent-author.md +++ b/nodejs/docs/agent-author.md @@ -122,10 +122,12 @@ hooks: { onSessionStart: async (input, invocation) => { ... }, onSessionEnd: async (input, invocation) => { ... }, onErrorOccurred: async (input, invocation) => { ... }, + onSubagentStart: async (input, invocation) => { ... }, + onSubagentStop: async (input, invocation) => { ... }, } ``` -All hook inputs include `timestamp` (`Date`) and `workingDirectory`. +All hook inputs include `sessionId`, `timestamp` (`Date`) and `workingDirectory`. All handlers receive `invocation: { sessionId: string }` as the second argument. All handlers may return `void`/`undefined` (no-op) or an output object. @@ -214,7 +216,32 @@ fire it. | `retryCount` | `number` | Max retries (when errorHandling is "retry") | | `userNotification` | `string` | Message shown to the user | ---- +### onSubagentStart + +Fires before a sub-agent's first turn. The input's `sessionId` and the +invocation's `sessionId` identify the parent session, not the child. + +**Input:** `{ sessionId: string, transcriptPath: string, agentName: string, agentDisplayName?: string, agentDescription?: string, timestamp, workingDirectory }` + +**Output (optional):** +| Field | Type | Effect | +|-------|------|--------| +| `additionalContext` | `string` | Prepended to the child's initial prompt | + +### onSubagentStop + +Fires after a sub-agent completes a turn. The input includes the child's last +assistant `response` and the parent's session metadata. `agentId` is available +when the task registry supplies one. This is distinct from `onAgentStop`, which +only runs for the top-level agent. + +**Input:** `{ sessionId: string, transcriptPath: string, agentName: string, agentDisplayName?: string, agentDescription?: string, agentId?: string, agentType: string, stopReason: "end_turn", response: string, timestamp, workingDirectory }` + +**Output (choose one, or return nothing):** +| Field | Type | Effect | +|-------|------|--------| +| `decision` and `reason` | `"block"` and `string` | Continue the child for another turn using `reason` | +| `modifiedResponse` | `string` | Replace the child's response reported to the parent | ## Session Object diff --git a/nodejs/esbuild-copilotsdk-nodejs.ts b/nodejs/esbuild-copilotsdk-nodejs.ts index f65a47236f..d3c5030a4e 100644 --- a/nodejs/esbuild-copilotsdk-nodejs.ts +++ b/nodejs/esbuild-copilotsdk-nodejs.ts @@ -34,4 +34,4 @@ import { writeFileSync } from "fs"; writeFileSync("dist/cjs/package.json", JSON.stringify({ type: "commonjs" }) + "\n"); // Generate .d.ts files -execSync("tsc", { stdio: "inherit" }); +execSync("tsc --incremental --tsBuildInfoFile dist/tsconfig.tsbuildinfo", { stdio: "inherit" }); diff --git a/nodejs/package.json b/nodejs/package.json index 806aeefa6f..e6b1e9992c 100644 --- a/nodejs/package.json +++ b/nodejs/package.json @@ -5,7 +5,7 @@ "url": "https://github.com/github/copilot-sdk.git" }, "version": "0.0.0-dev", - "copilotCliVersion": "1.0.92-3", + "copilotCliVersion": "1.0.92-4", "description": "TypeScript SDK for programmatic control of GitHub Copilot CLI via JSON-RPC", "main": "./dist/cjs/index.js", "types": "./dist/index.d.ts", @@ -97,6 +97,7 @@ }, "files": [ "dist/**/*", + "!dist/tsconfig.tsbuildinfo", "docs/**/*", "README.md" ] diff --git a/nodejs/src/canvas.ts b/nodejs/src/canvas.ts index aeb1f00ec4..b7c2254c4f 100644 --- a/nodejs/src/canvas.ts +++ b/nodejs/src/canvas.ts @@ -49,7 +49,12 @@ export interface CanvasAction { description?: string; /** Optional JSON Schema for the action's `input` payload. */ inputSchema?: CanvasJsonSchema; - /** Required per-action dispatch handler. */ + /** + * Required per-action dispatch handler. The returned value becomes the + * `invoke_canvas_action` tool result. Return a `ToolResultObject` + * (with `binaryResultsForLlm`) to send text and images to the model, as a + * tool handler would; any other value is rendered to the model as JSON text. + */ handler: (ctx: CanvasProviderInvokeActionRequest) => Promise | unknown; } diff --git a/nodejs/src/cliVersion.ts b/nodejs/src/cliVersion.ts index 987978a92f..93029c8524 100644 --- a/nodejs/src/cliVersion.ts +++ b/nodejs/src/cliVersion.ts @@ -1,3 +1,3 @@ -export const COPILOT_CLI_VERSION = "1.0.92-3"; +export const COPILOT_CLI_VERSION = "1.0.92-4"; export const COPILOT_CLI_USE_NPM_PACKAGE = false; diff --git a/nodejs/src/client.ts b/nodejs/src/client.ts index 858697fe8a..24f7f6ff2e 100644 --- a/nodejs/src/client.ts +++ b/nodejs/src/client.ts @@ -114,6 +114,7 @@ interface ExtensionOrchestrationContributions { */ const MIN_PROTOCOL_VERSION = 3; const RUNTIME_SHUTDOWN_TIMEOUT_MS = 10_000; +const CLOUD_SESSION_CLEANUP_TIMEOUT_MS = 10_000; type DrainingMessageConnection = MessageConnection & { drain: () => Promise }; @@ -899,6 +900,14 @@ export class CopilotClient { "SessionFsConfig declares capabilities.sqlite but the provider does not implement sqlite." ); } + if ( + this.sessionFsConfig.capabilities?.binary && + (!provider.readFileBytes || !provider.writeFileBytes) + ) { + throw new Error( + "SessionFsConfig declares capabilities.binary but the provider does not implement readFileBytes and writeFileBytes." + ); + } session.clientSessionApis.sessionFs = createSessionFsAdapter(provider); } @@ -1742,13 +1751,14 @@ export class CopilotClient { if (config.onEvent) { s.on(config.onEvent); } - this.sessions.set(sessionId, s); this.setupSessionFs(s, config); + this.sessions.set(sessionId, s); return s; }; let session: CopilotSession | undefined; let registeredId: string | undefined; + let uninitializedCloudSessionId: string | undefined; // Pre-register non-cloud sessions BEFORE issuing the RPC so any // session-scoped requests the CLI emits during `session.create` @@ -1888,7 +1898,9 @@ export class CopilotClient { if (session === undefined) { // Cloud / server-assigned path: register the session now that // the CLI has told us which id it chose. + uninitializedCloudSessionId = returnedSessionId; session = initializeSession(returnedSessionId); + uninitializedCloudSessionId = undefined; registeredId = returnedSessionId; } this.assignGitHubTokenProvider(gitHubTokenProviderRegistrationId, returnedSessionId); @@ -1911,6 +1923,20 @@ export class CopilotClient { if (gitHubTokenProviderRegistrationId !== undefined) { this.githubTokenProviders.delete(gitHubTokenProviderRegistrationId); } + if (uninitializedCloudSessionId !== undefined) { + try { + await withTimeout( + this.deleteSession(uninitializedCloudSessionId), + CLOUD_SESSION_CLEANUP_TIMEOUT_MS, + `session.delete timed out after ${CLOUD_SESSION_CLEANUP_TIMEOUT_MS}ms` + ); + } catch (cleanupError) { + throw new AggregateError( + [e, cleanupError], + "Failed to initialize and delete cloud session" + ); + } + } throw e; } @@ -2031,8 +2057,8 @@ export class CopilotClient { if (config.onEvent) { session.on(config.onEvent); } - this.sessions.set(sessionId, session); this.setupSessionFs(session, config); + this.sessions.set(sessionId, session); const toolFilterOptions = this.resolveToolFilterOptions(config); const gitHubTokenProviderRegistrationId = this.registerGitHubTokenProvider( diff --git a/nodejs/src/generated/rpc.ts b/nodejs/src/generated/rpc.ts index af073a9d0c..b423be039f 100644 --- a/nodejs/src/generated/rpc.ts +++ b/nodejs/src/generated/rpc.ts @@ -1472,6 +1472,46 @@ export type ContentFilterMode = | "markdown" /** Remove characters that can hide directives. */ | "hidden_characters"; +/** + * Component of session customization discovery. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "CustomizationReloadSubsystem". + */ +/** @experimental */ +export type CustomizationReloadSubsystem = + /** Repository metadata and working-directory context. */ + | "repositoryContext" + /** Session instructions and their cached dynamic context. */ + | "instructions" + /** Discovered plugin configuration. */ + | "plugins" + /** Configured session and plugin hooks. */ + | "hooks" + /** Discovered skills. */ + | "skills" + /** Discovered custom agents. */ + | "agents" + /** Loaded MCP server configuration. */ + | "mcp" + /** Configured session extensions. */ + | "extensions" + | (string & {}); +/** + * Result of reloading a customization component. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "CustomizationReloadStatus". + */ +/** @experimental */ +export type CustomizationReloadStatus = + /** The component was refreshed successfully. */ + | "reloaded" + /** The component was not configured, loaded, or eligible for refresh. */ + | "skipped" + /** The component could not be refreshed; other components may still reload. */ + | "failed" + | (string & {}); /** * Source category for a collected debug bundle entry. * @@ -1780,59 +1820,45 @@ export type DiscoveredMcpServerType = /** Server is backed by an in-memory runtime implementation. */ | "memory"; /** - * Indicates whether the command was accepted into the local execution queue. + * Kind of component that supplied a provider adapter or row. Attribution does not confer authority. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "EnqueueCommandResult". + * via the `definition` "ModelProviderProvenanceSource". */ /** @experimental */ -export type EnqueueCommandResult = AcceptedEnqueueCommandResult | UnsupportedEnqueueCommandResult; +export type ModelProviderProvenanceSource = + /** Built into the runtime. */ + | "builtIn" + /** Derived from existing user configuration. */ + | "configured" + /** Supplied by an extension. */ + | "extension" + /** Supplied by another trusted contributor. */ + | "custom"; /** - * How far OneAuth may go to acquire the requested token. + * Typed outcome for a provider operation. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "EntraTokenInteraction". + * via the `definition` "ModelProviderOperationOutcomeCode". */ /** @experimental */ -export type EntraTokenInteraction = - /** Acquire the token without any user interaction, failing if interaction would be required. */ - | "silent" - /** Allow interactive acquisition, prompting the user only when a cached or silent token is unavailable. */ - | "interactive" - /** Always prompt interactively, bypassing any cached or silently-refreshable token. */ - | "force-interactive"; +export type ModelProviderOperationOutcomeCode = + /** The operation completed successfully; an empty inventory is valid. */ + | "success" + /** The provider or instance is absent during discovery, status, or model listing. Distinct from a successful empty inventory. */ + | "absent" + /** The provider is configured or expected but could not be reached. */ + | "unreachable" + /** The operation failed for a reason other than absence or reachability. */ + | "failed"; /** - * Result of a OneAuth token acquisition. + * Indicates whether the command was accepted into the local execution queue. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "EntraTokenAcquireResult". + * via the `definition` "EnqueueCommandResult". */ /** @experimental */ -export type EntraTokenAcquireResult = - | { - /** - * Opaque access token. - */ - accessToken: string; - /** - * Expiry as milliseconds since Unix epoch, when supplied by OneAuth. - */ - expiresOnTimestamp?: number; - /** - * Opaque OneAuth account id, when supplied by the broker. - */ - accountId?: string; - /** - * OneAuth token acquisition outcome discriminator. - */ - status: "ok"; - } - | { - /** - * OneAuth token acquisition outcome discriminator. - */ - status: "interaction-required"; - }; +export type EnqueueCommandResult = AcceptedEnqueueCommandResult | UnsupportedEnqueueCommandResult; /** * GitHub Mission Control compute kind. * @@ -3773,17 +3799,49 @@ export type ModelListRequest = skipCache?: boolean; }; /** - * Whether the requested preference was already effective or was accepted for later transactional activation. + * When the runtime may run an adapter without an explicit user action. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "ModelSwitchAutoTierStatus". + * via the `definition` "ModelProviderAutomaticDiscoveryMode". */ /** @experimental */ -export type ModelSwitchAutoTierStatus = - /** The requested preference is already effective. No activation is pending for it, although this request may have cancelled an earlier unclaimed preference reported in `supersededAutoTier`. */ - | "unchanged" - /** The request was accepted but has not committed. A later user turn using the `auto` model must mint and validate the replacement before it becomes effective. */ - | "pending"; +export type ModelProviderAutomaticDiscoveryMode = + /** The adapter declares that automatic discovery is safe when the other policy fields are satisfied. */ + | "automatic" + /** The adapter may refresh instances the user already configured, but must not scan for new instances automatically. */ + | "configuredOnly" + /** The adapter must run only after an explicit user action. */ + | "explicit"; +/** + * Network reach an adapter may use during discovery. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "ModelProviderDiscoveryNetworkScope". + */ +/** @experimental */ +export type ModelProviderDiscoveryNetworkScope = + /** Discovery does not contact a network service. */ + | "none" + /** Discovery is limited to loopback addresses on the local machine. */ + | "loopbackOnly" + /** Discovery contacts only endpoints the user already configured. */ + | "configuredEndpointOnly" + /** Discovery may scan or contact the local network. */ + | "localNetwork" + /** Discovery may contact remote internet services. */ + | "internet"; +/** + * Whether a planned configuration entry is new or already present in the session registry. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "ModelProviderConfigurationDisposition". + */ +/** @experimental */ +export type ModelProviderConfigurationDisposition = + /** No matching entry is registered; the caller should add the entry. */ + | "create" + /** An equivalent entry is already registered; the caller should reuse it rather than adding a duplicate. */ + | "alreadyConfigured"; /** * Provider type. Defaults to "openai" for generic OpenAI-compatible APIs. * @@ -3844,6 +3902,100 @@ export type ProviderConfigModelProvider = | "foundry_local" /** llama.cpp server. */ | "llama_cpp"; + +/** @experimental */ +export type ProtocolSystemMessageConfig = + | ProtocolSystemMessageAppendConfig + | ProtocolSystemMessageReplaceConfig + | ProtocolSystemMessageCustomizeConfig; + +/** @experimental */ +export type ProtocolAppendMode = "append"; + +/** @experimental */ +export type ProtocolReplaceMode = "replace"; + +/** @experimental */ +export type ProtocolCustomizeMode = "customize"; + +/** @experimental */ +export type ProtocolSectionOverride = (ProtocolStaticSectionOverride | ProtocolMarkerSectionOverride) | undefined; + +/** @experimental */ +export type ProtocolStaticSectionAction = + /** Replace the section content. */ + | "replace" + /** Remove the section content. */ + | "remove" + /** Append content to the section. */ + | "append" + /** Prepend content to the section. */ + | "prepend"; + +/** @experimental */ +export type ProtocolMarkerSectionOverride = + | { + /** + * Section override action discriminator. + */ + action: "transform"; + } + | { + /** + * Section override action discriminator. + */ + action: "preserve"; + }; +/** + * Provider family. Matches the `type` field of a BYOK provider config. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "ProviderEndpointType". + */ +/** @experimental */ +export type ProviderEndpointType = + /** OpenAI-compatible endpoint (use the OpenAI client library). */ + | "openai" + /** Azure OpenAI endpoint (use the OpenAI client library with the Azure base URL). */ + | "azure" + /** Anthropic endpoint (use the Anthropic client library). */ + | "anthropic"; +/** + * Wire API to be used, when required for the provider type. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "ProviderEndpointWireApi". + */ +/** @experimental */ +export type ProviderEndpointWireApi = + /** Classic chat-completions request shape. */ + | "completions" + /** Newer responses request shape. */ + | "responses"; +/** + * Transport to be used for provider requests. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "ProviderEndpointTransport". + */ +/** @experimental */ +export type ProviderEndpointTransport = + /** HTTP request/streaming transport. */ + | "http" + /** WebSocket transport. */ + | "websockets"; +/** + * Whether the requested preference was already effective or was accepted for later transactional activation. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "ModelSwitchAutoTierStatus". + */ +/** @experimental */ +export type ModelSwitchAutoTierStatus = + /** The requested preference is already effective. No activation is pending for it, although this request may have cancelled an earlier unclaimed preference reported in `supersededAutoTier`. */ + | "unchanged" + /** The request was accepted but has not committed. A later user turn using the `auto` model must mint and validate the replacement before it becomes effective. */ + | "pending"; /** * Allowed values for the `OptionsUpdateAdditionalContentExclusionPolicyScope` enumeration. * @@ -4152,12 +4304,6 @@ export type PluginsReloadRequest = */ deferRepoHooks?: boolean; }; - -/** @experimental */ -export type ProtocolAppendMode = "append"; - -/** @experimental */ -export type ProtocolCustomizeMode = "customize"; /** * Controls whether the runtime may defer loading an external tool definition. * @@ -4170,82 +4316,6 @@ export type ProtocolExternalToolDefer = | "auto" /** The runtime must include the tool without deferring it. */ | "never"; - -/** @experimental */ -export type ProtocolMarkerSectionOverride = - | { - /** - * Section override action discriminator. - */ - action: "transform"; - } - | { - /** - * Section override action discriminator. - */ - action: "preserve"; - }; - -/** @experimental */ -export type ProtocolReplaceMode = "replace"; - -/** @experimental */ -export type ProtocolSectionOverride = ProtocolStaticSectionOverride | ProtocolMarkerSectionOverride; - -/** @experimental */ -export type ProtocolStaticSectionAction = - /** Replace the section content. */ - | "replace" - /** Remove the section content. */ - | "remove" - /** Append content to the section. */ - | "append" - /** Prepend content to the section. */ - | "prepend"; - -/** @experimental */ -export type ProtocolSystemMessageConfig = - | ProtocolSystemMessageAppendConfig - | ProtocolSystemMessageReplaceConfig - | ProtocolSystemMessageCustomizeConfig; -/** - * Provider family. Matches the `type` field of a BYOK provider config. - * - * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "ProviderEndpointType". - */ -/** @experimental */ -export type ProviderEndpointType = - /** OpenAI-compatible endpoint (use the OpenAI client library). */ - | "openai" - /** Azure OpenAI endpoint (use the OpenAI client library with the Azure base URL). */ - | "azure" - /** Anthropic endpoint (use the Anthropic client library). */ - | "anthropic"; -/** - * Wire API to be used, when required for the provider type. - * - * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "ProviderEndpointWireApi". - */ -/** @experimental */ -export type ProviderEndpointWireApi = - /** Classic chat-completions request shape. */ - | "completions" - /** Newer responses request shape. */ - | "responses"; -/** - * Transport to be used for provider requests. - * - * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "ProviderEndpointTransport". - */ -/** @experimental */ -export type ProviderEndpointTransport = - /** HTTP request/streaming transport. */ - | "http" - /** WebSocket transport. */ - | "websockets"; /** * Optional model identifier to scope the endpoint snapshot to. * @@ -4438,6 +4508,22 @@ export type SandboxConfigSource = */ /** @experimental */ export type SandboxHostCapabilityName = string; +/** + * State of the persistent certificate authority of the sandbox credential proxy. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "SandboxProxyCaState". + */ +/** @experimental */ +export type SandboxProxyCaState = + /** This platform has no supported OS trust store. The proxy uses a per-process certificate bundle. */ + | "unsupported" + /** OS trust does not include the certificate authority, or none is stored. */ + | "notInstalled" + /** OS trust includes the stored certificate authority. */ + | "installed" + /** The runtime could not read the certificate authority or the OS trust store. */ + | "error"; /** * A session-scoped sandbox transition applied while handling a slash command * @@ -9933,6 +10019,10 @@ export interface CurrentModel { * Currently active model identifier */ modelId?: string; + /** + * Captured base model to restore when leaving plan mode. Omitted outside plan mode or when no plan override has captured a base model. Persistent agent model requirements apply to this model rather than the temporary plan model. + */ + planBaseModelId?: string; /** * Reasoning effort level currently applied to the active model, when one is set. Reads `Session.getReasoningEffort()` synchronously after `getSelectedModel()` resolves so the two values are reported as a snapshot. */ @@ -9987,6 +10077,42 @@ export interface CurrentToolMetadata { */ deferLoading?: boolean; } +/** + * Result of one customization reload component. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "CustomizationReloadOutcome". + */ +/** @experimental */ +export interface CustomizationReloadOutcome { + subsystem: CustomizationReloadSubsystem; + status: CustomizationReloadStatus; + /** + * Reason for a skipped component or description of a failure, when available + */ + detail?: string | null; +} +/** + * Results of reloading discovered session customizations. Inspect outcomes for reloaded, skipped, or failed subsystems; a rejection may follow partial mutation. Changes to the model-facing prompt and tools apply on the next turn. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "CustomizationsReloadResult". + */ +/** @experimental */ +export interface CustomizationsReloadResult { + /** + * Warnings from skill discovery + */ + warnings: string[]; + /** + * Errors from any component that could not be refreshed + */ + errors: string[]; + /** + * Outcome of each component in reload order; a skipped component was not configured or loaded + */ + outcomes: CustomizationReloadOutcome[]; +} /** * A file included in the session debug bundle. * @@ -10438,65 +10564,257 @@ export interface McpSourcePlugin { version?: string; } /** - * Slash-prefixed command string to enqueue for FIFO processing. + * A model offered for agent conversations. Missing capability metadata does not disqualify a candidate. Models known to be incompatible, such as embedding-only models, are excluded by the adapter. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "EnqueueCommandParams". + * via the `definition` "DiscoveredModel". */ /** @experimental */ -export interface EnqueueCommandParams { +export interface DiscoveredModel { /** - * Slash-prefixed command string to enqueue, e.g. '/compact' or '/model gpt-4'. Queued FIFO with any in-flight items; if the session is idle, processing kicks off immediately. + * Provider-native model identifier. */ - command: string; + id: string; /** - * Optional user-facing text for the queue row. The command string is shown when omitted. + * Provider-reported display name. */ - displayText?: string | null; + name?: string; + provenance: ModelProviderProvenance; + /** + * Provider-reported artifact digest. + */ + digest?: string; + /** + * Provider-reported last-modified timestamp. + */ + modifiedAt?: string; + /** + * Provider-reported artifact size in bytes. + */ + sizeBytes?: number; + details: ModelArtifactDetails; + capabilities: ModelCapabilities; + /** + * Non-fatal warnings encountered while enriching this model. + */ + warnings: ModelProviderWarning[]; } - +/** + * Attribution for the adapter that produced a provider row. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "ModelProviderProvenance". + */ /** @experimental */ -export interface UnsupportedEnqueueCommandResult { +export interface ModelProviderProvenance { /** - * False because the target session does not support local command queueing (e.g. remote sessions). + * Stable opaque adapter identity from the effective catalog. Treat this as a whole identifier, not a parseable owner or kind. */ - queued: false; + adapterId: string; /** - * Legacy null queue ID accepted for compatibility with older runtimes. + * Descriptive provider family that produced this row; not a routing key. */ - queueId?: null; + providerKind: string; + source: ModelProviderProvenanceSource; + /** + * Stable contributor identifier when the adapter has an owner outside the runtime. Independent of the contribution mechanism and not a routing key. + */ + ownerId?: string; + /** + * Human-readable contributor name, not the adapter display name. + */ + ownerDisplayName?: string; } /** - * OneAuth token request supplied by a trusted host application. + * Provider-reported model artifact metadata. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "EntraTokenAcquireRequest". + * via the `definition` "ModelArtifactDetails". */ /** @experimental */ -export interface EntraTokenAcquireRequest { +export interface ModelArtifactDetails { /** - * Public client application id. + * Artifact format, such as `gguf`. */ - clientId: string; + format?: string; /** - * Tenant id or tenant selector, such as common or organizations. + * Primary model family. */ - tenantId: string; + family?: string; /** - * Broker redirect URI registered for the client. Required: the OneAuth broker validates a non-empty, registered redirect URI for the public client (MSAL broker registration), so this is not a browser-flow vestige and cannot be omitted. + * Provider-reported model families. */ - redirectUri: string; + families?: string[]; /** - * Exact delegated scopes to request. - * - * @minItems 1 + * Provider-reported parameter count label. + */ + parameterSize?: string; + /** + * Provider-reported quantization label. + */ + quantization?: string; + /** + * Provider-reported model architecture. + */ + architecture?: string; + /** + * Provider-reported tokenizer. + */ + tokenizer?: string; +} +/** + * Model capabilities and limits + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "ModelCapabilities". + */ +/** @experimental */ +export interface ModelCapabilities { + supports?: ModelCapabilitiesSupports; + limits?: ModelCapabilitiesLimits; +} +/** + * Feature flags indicating what the model supports + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "ModelCapabilitiesSupports". + */ +/** @experimental */ +export interface ModelCapabilitiesSupports { + /** + * Whether the model supports provider-native thinking. Independent of configurable reasoning effort; omission means unknown. + */ + thinking?: boolean; + /** + * Whether this model supports vision/image input + */ + vision?: boolean; + /** + * Whether this model supports canonical tool calling + */ + toolCalls?: boolean; + /** + * Whether this model supports reasoning effort configuration + */ + reasoningEffort?: boolean; + adaptive_thinking?: AdaptiveThinkingSupport; +} +/** + * Token limits for prompts, outputs, and context window + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "ModelCapabilitiesLimits". + */ +/** @experimental */ +export interface ModelCapabilitiesLimits { + /** + * Maximum number of prompt/input tokens + */ + max_prompt_tokens?: number; + /** + * Maximum number of output/completion tokens + */ + max_output_tokens?: number; + /** + * Maximum total context window size in tokens + */ + max_context_window_tokens?: number; + vision?: ModelCapabilitiesLimitsVision; +} +/** + * Vision-specific limits + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "ModelCapabilitiesLimitsVision". + */ +/** @experimental */ +export interface ModelCapabilitiesLimitsVision { + /** + * MIME types the model accepts + */ + supported_media_types: string[]; + /** + * Maximum number of images per prompt + */ + max_prompt_images: number; + /** + * Maximum image size in bytes + */ + max_prompt_image_size: number; +} +/** + * A non-fatal provider observation warning. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "ModelProviderWarning". + */ +/** @experimental */ +export interface ModelProviderWarning { + /** + * Machine-readable warning code. + */ + code: string; + /** + * Human-readable warning message. + */ + message: string; +} +/** + * Models offered for agent conversations by one provider instance. Adapters exclude known-incompatible models, but retain candidates with unknown capabilities. Listing does not guarantee compatibility. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "DiscoveredModelList". + */ +/** @experimental */ +export interface DiscoveredModelList { + outcome: ModelProviderOperationOutcome; + /** + * Provider-native models in provider order. + */ + models: DiscoveredModel[]; +} +/** + * Typed provider-operation outcome. Use the code for control flow and the optional message for display. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "ModelProviderOperationOutcome". + */ +/** @experimental */ +export interface ModelProviderOperationOutcome { + code: ModelProviderOperationOutcomeCode; + /** + * Human-readable detail for non-success outcomes. */ - scopes: [string, ...string[]]; - interaction: EntraTokenInteraction; + message?: string; +} +/** + * Slash-prefixed command string to enqueue for FIFO processing. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "EnqueueCommandParams". + */ +/** @experimental */ +export interface EnqueueCommandParams { /** - * Previously rejected token that OneAuth must bypass during renewal. + * Slash-prefixed command string to enqueue, e.g. '/compact' or '/model gpt-4'. Queued FIFO with any in-flight items; if the session is idle, processing kicks off immediately. */ - accessTokenToRenew?: string; + command: string; + /** + * Optional user-facing text for the queue row. The command string is shown when omitted. + */ + displayText?: string | null; +} + +/** @experimental */ +export interface UnsupportedEnqueueCommandResult { + /** + * False because the target session does not support local command queueing (e.g. remote sessions). + */ + queued: false; + /** + * Legacy null queue ID accepted for compatibility with older runtimes. + */ + queueId?: null; } /** * Hosting capabilities and session capacity advertised by an environment. @@ -16589,170 +16907,94 @@ export interface Model { provider?: ModelProviderRef; } /** - * Model capabilities and limits + * Policy state (if applicable) * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "ModelCapabilities". + * via the `definition` "ModelPolicy". */ /** @experimental */ -export interface ModelCapabilities { - supports?: ModelCapabilitiesSupports; - limits?: ModelCapabilitiesLimits; +export interface ModelPolicy { + state: ModelPolicyState; + /** + * Usage terms or conditions for this model + */ + terms?: string; } /** - * Feature flags indicating what the model supports + * Billing information * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "ModelCapabilitiesSupports". + * via the `definition` "ModelBilling". */ /** @experimental */ -export interface ModelCapabilitiesSupports { - /** - * Whether this model supports vision/image input - */ - vision?: boolean; +export interface ModelBilling { /** - * Whether this model supports canonical tool calling + * Billing cost multiplier relative to the base rate */ - toolCalls?: boolean; + multiplier?: number; + tokenPrices?: ModelBillingTokenPrices; /** - * Whether this model supports reasoning effort configuration + * Whole-number percentage discount (0-100) applied to usage billed through this model. Populated for the synthetic `auto` model, where requests routed by auto-mode are billed at a reduced rate; absent for concrete models. */ - reasoningEffort?: boolean; - adaptive_thinking?: AdaptiveThinkingSupport; + discountPercent?: number; + promo?: ModelBillingPromo; } /** - * Token limits for prompts, outputs, and context window + * Token-level pricing information for this model * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "ModelCapabilitiesLimits". + * via the `definition` "ModelBillingTokenPrices". */ /** @experimental */ -export interface ModelCapabilitiesLimits { +export interface ModelBillingTokenPrices { /** - * Maximum number of prompt/input tokens + * AI Credits cost per billing batch of input tokens */ - max_prompt_tokens?: number; + inputPrice?: number; /** - * Maximum number of output/completion tokens + * AI Credits cost per billing batch of output tokens */ - max_output_tokens?: number; + outputPrice?: number; /** - * Maximum total context window size in tokens + * @deprecated + * Use cacheReadPrice instead. AI Credits cost per billing batch of cached tokens */ - max_context_window_tokens?: number; - vision?: ModelCapabilitiesLimitsVision; -} -/** - * Vision-specific limits - * - * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "ModelCapabilitiesLimitsVision". - */ -/** @experimental */ -export interface ModelCapabilitiesLimitsVision { + cachePrice?: number; /** - * MIME types the model accepts + * AI Credits cost per billing batch of cached (read) tokens */ - supported_media_types: string[]; + cacheReadPrice?: number; /** - * Maximum number of images per prompt + * AI Credits cost per billing batch of cache-write (cache creation) tokens. */ - max_prompt_images: number; + cacheWritePrice?: number; /** - * Maximum image size in bytes + * AI Credits cost per billing batch of 1-hour cache-write (cache creation) tokens. */ - max_prompt_image_size: number; -} -/** - * Policy state (if applicable) - * - * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "ModelPolicy". - */ -/** @experimental */ -export interface ModelPolicy { - state: ModelPolicyState; + cacheWrite1hPrice?: number; /** - * Usage terms or conditions for this model + * Number of tokens per standard billing batch */ - terms?: string; -} -/** - * Billing information - * - * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "ModelBilling". - */ -/** @experimental */ -export interface ModelBilling { + batchSize?: number; /** - * Billing cost multiplier relative to the base rate + * @deprecated + * Use maxPromptTokens instead. Prompt token budget for the default tier. The total context window is this value plus the model's max_output_tokens. */ - multiplier?: number; - tokenPrices?: ModelBillingTokenPrices; + contextMax?: number; /** - * Whole-number percentage discount (0-100) applied to usage billed through this model. Populated for the synthetic `auto` model, where requests routed by auto-mode are billed at a reduced rate; absent for concrete models. + * Prompt token budget for the default tier. The total context window is this value plus the model's max_output_tokens. */ - discountPercent?: number; - promo?: ModelBillingPromo; + maxPromptTokens?: number; + longContext?: ModelBillingTokenPricesLongContext; } /** - * Token-level pricing information for this model + * Long context tier pricing (available for models with extended context windows) * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "ModelBillingTokenPrices". + * via the `definition` "ModelBillingTokenPricesLongContext". */ /** @experimental */ -export interface ModelBillingTokenPrices { - /** - * AI Credits cost per billing batch of input tokens - */ - inputPrice?: number; - /** - * AI Credits cost per billing batch of output tokens - */ - outputPrice?: number; - /** - * @deprecated - * Use cacheReadPrice instead. AI Credits cost per billing batch of cached tokens - */ - cachePrice?: number; - /** - * AI Credits cost per billing batch of cached (read) tokens - */ - cacheReadPrice?: number; - /** - * AI Credits cost per billing batch of cache-write (cache creation) tokens. - */ - cacheWritePrice?: number; - /** - * AI Credits cost per billing batch of 1-hour cache-write (cache creation) tokens. - */ - cacheWrite1hPrice?: number; - /** - * Number of tokens per standard billing batch - */ - batchSize?: number; - /** - * @deprecated - * Use maxPromptTokens instead. Prompt token budget for the default tier. The total context window is this value plus the model's max_output_tokens. - */ - contextMax?: number; - /** - * Prompt token budget for the default tier. The total context window is this value plus the model's max_output_tokens. - */ - maxPromptTokens?: number; - longContext?: ModelBillingTokenPricesLongContext; -} -/** - * Long context tier pricing (available for models with extended context windows) - * - * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "ModelBillingTokenPricesLongContext". - */ -/** @experimental */ -export interface ModelBillingTokenPricesLongContext { +export interface ModelBillingTokenPricesLongContext { /** * AI Credits cost per billing batch of input tokens */ @@ -17042,6 +17284,285 @@ export interface ModelPickerSettingsContext { */ environment: {}; } +/** + * Normalized model-provider adapter definitions available to the session, not discovered instances. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "ModelProviderAdapterCatalog". + */ +/** @experimental */ +export interface ModelProviderAdapterCatalog { + /** + * Available provider adapters ordered by adapterId. + */ + providers: ModelProviderAdapterDescriptor[]; +} +/** + * A normalized model-provider adapter in the session's effective catalog. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "ModelProviderAdapterDescriptor". + */ +/** @experimental */ +export interface ModelProviderAdapterDescriptor { + /** + * Stable opaque identity for routing to this adapter. Unique in the effective catalog, independent of live registration generations. + */ + adapterId: string; + /** + * Descriptive provider family, such as `ollama`. Different adapters may have the same family; use adapterId for routing. + */ + providerKind: string; + /** + * Human-readable provider name. + */ + displayName: string; + provenance: ModelProviderAttribution; + automaticDiscovery: ModelProviderAutomaticDiscoveryPolicy; + /** + * Operations supported by this provider adapter. + */ + operations: ModelProviderAdapterOperationDescriptor[]; +} +/** + * Contributor attribution, independent of routing identity and authorization. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "ModelProviderAttribution". + */ +/** @experimental */ +export interface ModelProviderAttribution { + source: ModelProviderProvenanceSource; + /** + * Stable contributor identifier. Required and nonblank for extension and custom sources; optional for built-in and configured sources. Does not grant authority. + */ + ownerId?: string; + /** + * Human-readable contributor name, not the adapter display name. + */ + ownerDisplayName?: string; +} +/** + * Adapter-declared policy that tells clients whether discovery may run automatically. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "ModelProviderAutomaticDiscoveryPolicy". + */ +/** @experimental */ +export interface ModelProviderAutomaticDiscoveryPolicy { + mode: ModelProviderAutomaticDiscoveryMode; + networkScope: ModelProviderDiscoveryNetworkScope; + /** + * True when discovery requires non-null caller input. Omission or null is rejected before adapter execution. When false, omitted or null input selects adapter defaults without schema validation. + */ + requiresInput: boolean; + /** + * True when the adapter must be enabled by a trusted owner, such as a trusted extension, before automatic discovery may run. + */ + requiresTrust: boolean; +} +/** + * An operation supported by a model-provider adapter. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "ModelProviderAdapterOperationDescriptor". + */ +/** @experimental */ +export interface ModelProviderAdapterOperationDescriptor { + /** + * Supported operation name: `discover`, `getStatus`, or `models.list`. Unknown names and duplicate declarations are rejected. + */ + name: string; + /** + * Optional self-contained JSON Schema Draft 7 for non-null discovery input. Only supported on discover. No external references are resolved. Omitted or null input selects defaults when requiresInput is false. Without a schema, the adapter validates supplied input. + */ + inputSchema?: JsonValue; +} +/** + * Provider configuration prepared from a discovered model. Preparing a plan changes nothing: it neither registers the model with the session nor writes durable configuration. To apply it, pass `provider` and `model` to `session.provider.add`, omitting whichever the dispositions report as already configured. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "ModelProviderConfigurationPlan". + */ +/** @experimental */ +export interface ModelProviderConfigurationPlan { + provider: NamedProviderConfig; + model: ProviderModelConfig; + /** + * Provider-qualified selection id (`provider/id`) to pass to `switchTo` once the plan is applied. + */ + selectionId: string; + providerDisposition: ModelProviderConfigurationDisposition; + modelDisposition: ModelProviderConfigurationDisposition; + /** + * Non-fatal warnings carried over from the discovered model, such as capabilities the provider did not report. + */ + warnings: ModelProviderWarning[]; +} +/** + * External SDK input for a named custom model provider. Ingested by the native protocol boundary before host dispatch. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "NamedProviderConfig". + */ +/** @experimental */ +export interface NamedProviderConfig { + /** + * Unique provider name used to qualify model selection IDs. + */ + name: string; + type?: ProviderConfigType; + wireApi?: ProviderConfigWireApi; + transport?: ProviderConfigTransport; + /** + * Base URL for provider API requests. + */ + baseUrl: string; + /** + * Static API key used to authenticate provider requests. + */ + apiKey?: string; + /** + * Static bearer token used to authenticate provider requests. + */ + bearerToken?: string; + azure?: ProviderConfigAzure; + /** + * Additional HTTP headers included with provider requests. + */ + headers?: { + [k: string]: string | undefined; + }; + modelProvider?: ProviderConfigModelProvider; + /** + * Whether the host supplies bearer tokens dynamically. + */ + hasBearerTokenProvider?: boolean; +} +/** + * Azure-specific provider options. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "ProviderConfigAzure". + */ +/** @experimental */ +export interface ProviderConfigAzure { + /** + * API version. When set, uses the versioned deployment route. When omitted, uses the GA versionless v1 route. + */ + apiVersion?: string; +} +/** + * A BYOK model definition referencing a named provider. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "ProviderModelConfig". + */ +/** @experimental */ +export interface ProviderModelConfig { + /** + * Provider-local model id, unique within its provider. The session-wide selection id (shown in the model list and passed to switchTo) is the provider-qualified `provider/id`. + */ + id: string; + /** + * Name of the configured provider that serves this model. + */ + provider: string; + /** + * The model name sent to the provider API for inference. Defaults to `id`. + */ + wireModel?: string; + /** + * Well-known base model id used for behavior/capability/config lookup. Defaults to `id`. + */ + modelId?: string; + /** + * Display name for model pickers. Defaults to the provider-qualified selection id (`provider/id`). + */ + name?: string; + /** + * Maximum prompt/input tokens for the model. + */ + maxPromptTokens?: number; + /** + * Maximum context window tokens for the model. + */ + maxContextWindowTokens?: number; + /** + * Maximum output tokens for the model. + */ + maxOutputTokens?: number; + capabilities?: ModelCapabilitiesOverride; + systemMessage?: ProtocolSystemMessageConfig; + /** + * Provider-published model metadata, preserved verbatim as the public Model.metadata object. + */ + metadata?: { + [k: string]: JsonValue | undefined; + }; +} + +/** @experimental */ +export interface ProtocolSystemMessageAppendConfig { + mode?: ProtocolAppendMode; + /** + * Text appended to the standard system prompt. + */ + content?: string; +} + +/** @experimental */ +export interface ProtocolSystemMessageReplaceConfig { + mode: ProtocolReplaceMode; + /** + * Complete replacement system-message text. + */ + content: string; + /** + * Optional structured blocks corresponding to the replacement content. + */ + contentBlocks?: SystemMessageBlock[]; +} + +/** @experimental */ +export interface SystemMessageBlock { + /** + * Text content for this system-message block. + */ + content: string; + /** + * Whether the block is static and may be cached independently of dynamic prompt content. + */ + isStatic?: boolean; + /** + * Whether providers with explicit prompt caching should place a cache breakpoint after this block. + */ + cacheBreakpoint?: boolean; +} + +/** @experimental */ +export interface ProtocolSystemMessageCustomizeConfig { + mode: ProtocolCustomizeMode; + /** + * Named standard-prompt section overrides. + */ + sections?: { + [k: string]: ProtocolSectionOverride | undefined; + }; + /** + * Text appended after the customized sections. + */ + content?: string; +} + +/** @experimental */ +export interface ProtocolStaticSectionOverride { + action: ProtocolStaticSectionAction; + /** + * Optional content used by replace, append, and prepend operations. + */ + content?: string; +} /** * One model provider available to the session — the model analog of the account `ProviderDescriptor`. Opaque id/label/kind plus a stable ordering; central code never branches on kind. * @@ -17064,6 +17585,134 @@ export interface ModelProviderDescriptor { */ ordering: number; } +/** + * Provider discovery parameters. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "ModelProviderDiscoverRequest". + */ +/** @experimental */ +export interface ModelProviderDiscoverRequest { + /** + * Opaque adapter identity returned by `session.providers.getCatalog`. + */ + adapterId: string; + /** + * Provider-specific JSON input. Omission or null selects adapter defaults unless requiresInput is true. Non-null input is validated against the advertised Draft 7 schema when present; otherwise validation belongs to the adapter. + */ + input?: JsonValue; +} +/** + * Provider instances found by a discovery operation. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "ModelProviderDiscoverResult". + */ +/** @experimental */ +export interface ModelProviderDiscoverResult { + outcome: ModelProviderOperationOutcome; + /** + * Discovered provider instances. Empty when passive default discovery finds no reachable provider. + */ + instances: ModelProviderInstance[]; +} +/** + * A normalized model-provider instance discovered by the runtime. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "ModelProviderInstance". + */ +/** @experimental */ +export interface ModelProviderInstance { + reference: ModelProviderInstanceReference; + /** + * Human-readable instance name. + */ + displayName: string; + provenance: ModelProviderProvenance; + /** + * Inference API endpoint when the provider exposes one separately from its management endpoint. + */ + inferenceEndpoint?: string; + inferenceType?: ProviderEndpointType; + inferenceWireApi?: ProviderEndpointWireApi; + inferenceTransport?: ProviderEndpointTransport; +} +/** + * Serializable reference to a discovered provider instance. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "ModelProviderInstanceReference". + */ +/** @experimental */ +export interface ModelProviderInstanceReference { + /** + * Stable opaque identity of the adapter that owns this reference. Must be present in the target session's effective catalog. + */ + adapterId: string; + /** + * Descriptive provider family. Must match the selected adapter; not a routing key. + */ + providerKind: string; + /** + * Stable instance identifier derived by the provider adapter, such as `ollama:{normalizedEndpoint}`. + */ + id: string; + /** + * Absolute provider management URI. The adapter validates normalization, supported schemes, and permission to access it against its bound configuration; a reference does not grant authority. + */ + managementEndpoint: string; +} +/** + * Provider status request parameters. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "ModelProviderGetStatusRequest". + */ +/** @experimental */ +export interface ModelProviderGetStatusRequest { + instance: ModelProviderInstanceReference; +} +/** + * Provider model inventory request parameters. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "ModelProviderModelsListRequest". + */ +/** @experimental */ +export interface ModelProviderModelsListRequest { + instance: ModelProviderInstanceReference; +} +/** + * A discovered instance and one of its models to translate into provider configuration. Pass back the instance and model as returned by `session.providers.discover` and `session.providers.models.list`. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "ModelProviderPrepareConfigurationRequest". + */ +/** @experimental */ +export interface ModelProviderPrepareConfigurationRequest { + instance: ModelProviderInstance; + model: DiscoveredModel; +} +/** + * Current health information for a provider instance. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "ModelProviderStatus". + */ +/** @experimental */ +export interface ModelProviderStatus { + outcome: ModelProviderOperationOutcome; + instance: ModelProviderInstance; + /** + * Open provider status value, such as `healthy`, `unreachable`, or `notInstalled`. + */ + status: string; + /** + * Provider-reported version. + */ + version?: string; +} /** * Host-supplied exact model selection IDs to allow for this running session. CAPI IDs are intersected with repository `.github/allowed_models.txt` policy; provider-qualified IDs remain exempt from repository-only policy but are restricted by this host list. Omit or pass null to clear the host restriction; an explicit empty or disjoint list is rejected. Validation and pre-selection fallback failures preserve the previous restriction. Failures after a fallback selection commits retain the new restriction and selected model; callers should inspect current session state after such an error. * @@ -17390,59 +18039,6 @@ export interface MoveMcpLoadingToBackgroundResult { */ movedToBackground: boolean; } -/** - * External SDK input for a named custom model provider. Ingested by the native protocol boundary before host dispatch. - * - * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "NamedProviderConfig". - */ -/** @experimental */ -export interface NamedProviderConfig { - /** - * Unique provider name used to qualify model selection IDs. - */ - name: string; - type?: ProviderConfigType; - wireApi?: ProviderConfigWireApi; - transport?: ProviderConfigTransport; - /** - * Base URL for provider API requests. - */ - baseUrl: string; - /** - * Static API key used to authenticate provider requests. - */ - apiKey?: string; - /** - * Static bearer token used to authenticate provider requests. - */ - bearerToken?: string; - azure?: ProviderConfigAzure; - /** - * Additional HTTP headers included with provider requests. - */ - headers?: { - [k: string]: string | undefined; - }; - modelProvider?: ProviderConfigModelProvider; - /** - * Whether the host supplies bearer tokens dynamically. - */ - hasBearerTokenProvider?: boolean; -} -/** - * Azure-specific provider options. - * - * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "ProviderConfigAzure". - */ -/** @experimental */ -export interface ProviderConfigAzure { - /** - * API version. When set, uses the versioned deployment route. When omitted, uses the GA versionless v1 route. - */ - apiVersion?: string; -} /** * The session's friendly name, or null when not yet set. * @@ -19580,68 +20176,6 @@ export interface ProtocolExternalToolDefinition { [k: string]: JsonValue | undefined; }; } - -/** @experimental */ -export interface ProtocolStaticSectionOverride { - action: ProtocolStaticSectionAction; - /** - * Optional content used by replace, append, and prepend operations. - */ - content?: string; -} - -/** @experimental */ -export interface ProtocolSystemMessageAppendConfig { - mode?: ProtocolAppendMode; - /** - * Text appended to the standard system prompt. - */ - content?: string; -} - -/** @experimental */ -export interface ProtocolSystemMessageReplaceConfig { - mode: ProtocolReplaceMode; - /** - * Complete replacement system-message text. - */ - content: string; - /** - * Optional structured blocks corresponding to the replacement content. - */ - contentBlocks?: SystemMessageBlock[]; -} - -/** @experimental */ -export interface SystemMessageBlock { - /** - * Text content for this system-message block. - */ - content: string; - /** - * Whether the block is static and may be cached independently of dynamic prompt content. - */ - isStatic?: boolean; - /** - * Whether providers with explicit prompt caching should place a cache breakpoint after this block. - */ - cacheBreakpoint?: boolean; -} - -/** @experimental */ -export interface ProtocolSystemMessageCustomizeConfig { - mode: ProtocolCustomizeMode; - /** - * Named standard-prompt section overrides. - */ - sections?: { - [k: string]: ProtocolSectionOverride; - }; - /** - * Text appended after the customized sections. - */ - content?: string; -} /** * BYOK providers and/or models to add to the session's registry at runtime. Both fields are optional; provide providers, models, or both. * @@ -19659,55 +20193,6 @@ export interface ProviderAddRequest { */ models?: ProviderModelConfig[]; } -/** - * A BYOK model definition referencing a named provider. - * - * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "ProviderModelConfig". - */ -/** @experimental */ -export interface ProviderModelConfig { - /** - * Provider-local model id, unique within its provider. The session-wide selection id (shown in the model list and passed to switchTo) is the provider-qualified `provider/id`. - */ - id: string; - /** - * Name of the configured provider that serves this model. - */ - provider: string; - /** - * The model name sent to the provider API for inference. Defaults to `id`. - */ - wireModel?: string; - /** - * Well-known base model id used for behavior/capability/config lookup. Defaults to `id`. - */ - modelId?: string; - /** - * Display name for model pickers. Defaults to the provider-qualified selection id (`provider/id`). - */ - name?: string; - /** - * Maximum prompt/input tokens for the model. - */ - maxPromptTokens?: number; - /** - * Maximum context window tokens for the model. - */ - maxContextWindowTokens?: number; - /** - * Maximum output tokens for the model. - */ - maxOutputTokens?: number; - capabilities?: ModelCapabilitiesOverride; - systemMessage?: ProtocolSystemMessageConfig; - /** - * Provider-published model metadata, preserved verbatim as the public Model.metadata object. - */ - metadata?: { - [k: string]: JsonValue | undefined; - }; -} /** * The selectable model entries synthesized for the models added by this call. * @@ -21609,6 +22094,47 @@ export interface SandboxHostSupport { */ capabilities: SandboxHostCapability[]; } +/** + * Result of creating the persistent certificate authority of the sandbox credential proxy. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "SandboxProxyCaCreateResult". + */ +/** @experimental */ +export interface SandboxProxyCaCreateResult { + /** + * Absolute path of the public certificate of the certificate authority, in PEM format. + */ + certificatePath: string; +} +/** + * Identifies the credential hosts that the persistent certificate authority of the sandbox credential proxy must cover. The runtime always adds the hosts from the saved user settings. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "SandboxProxyCaRequest". + */ +/** @experimental */ +export interface SandboxProxyCaRequest { + sandboxConfig?: SandboxConfig; +} +/** + * Status of the persistent certificate authority of the sandbox credential proxy. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "SandboxProxyCaStatus". + */ +/** @experimental */ +export interface SandboxProxyCaStatus { + state: SandboxProxyCaState; + /** + * Human-readable reason for the state. On `installed` or `notInstalled`, present only when the certificate authority must be rotated, and then says why. + */ + detail?: string; + /** + * Whether this process can add the certificate authority to OS trust without credentials from a different user. False where OS trust is unsupported, and on Windows when the process cannot elevate itself to write the machine trust store. When false, do not offer to set up the certificate authority. + */ + canInstall: boolean; +} /** * Register an absolute-time scheduled prompt. * @@ -22306,6 +22832,10 @@ export interface SessionFsError { * Free-form detail about the error, for logging/diagnostics */ message?: string; + /** + * For failed writeFile requests only: true if the provider changed the target before failing. Omit when unknown or unchanged. + */ + writeChanged?: boolean; } /** * Path to test for existence in the client-provided session filesystem. @@ -22438,6 +22968,37 @@ export interface SessionFsReaddirWithTypesResult { entries: SessionFsReaddirWithTypesEntry[]; error?: SessionFsError; } +/** + * Path of the binary file to read from the client-provided session filesystem. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "SessionFsReadFileBytesRequest". + */ +/** @experimental */ +export interface SessionFsReadFileBytesRequest { + /** + * Target session identifier + */ + sessionId: string; + /** + * Path using SessionFs conventions + */ + path: string; +} +/** + * File bytes as standard base64, or a filesystem error if the read failed. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "SessionFsReadFileBytesResult". + */ +/** @experimental */ +export interface SessionFsReadFileBytesResult { + /** + * Exact file bytes encoded as standard base64 + */ + content: string; + error?: SessionFsError; +} /** * Path of the file to read from the client-provided session filesystem. * @@ -22527,6 +23088,10 @@ export interface SessionFsSetProviderCapabilities { * Whether the provider supports SQLite query/exists operations */ sqlite?: boolean; + /** + * Whether the provider supports binary reads and writes through sessionFs.readFileBytes and sessionFs.writeFileBytes + */ + binary?: boolean; } /** * Initial working directory, session-state path layout, and path conventions used to register the calling SDK client as the session filesystem provider. A registered provider is authoritative for path interpretation and filesystem facts used by workspace permission validation. Paths are interpreted lexically; home-relative paths (`~` and `~/...`) and Windows drive-relative paths such as `C:foo` are unsupported. Until provider-side canonicalization is supported, providers must not expose symlinks inside allowed roots that escape those roots. @@ -22737,6 +23302,31 @@ export interface SessionFsStatResult { birthtime: string; error?: SessionFsError; } +/** + * File path, standard-base64-encoded bytes to write, and optional mode for the client-provided session filesystem. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "SessionFsWriteFileBytesRequest". + */ +/** @experimental */ +export interface SessionFsWriteFileBytesRequest { + /** + * Target session identifier + */ + sessionId: string; + /** + * Path using SessionFs conventions + */ + path: string; + /** + * Exact file bytes encoded as standard base64 + */ + content: string; + /** + * Optional POSIX-style mode for newly created files + */ + mode?: number; +} /** * File path, content to write, and optional mode for the client-provided session filesystem. * @@ -30036,6 +30626,52 @@ export function createServerRpc(connection: MessageConnection) { */ getHostSupport: async (): Promise => connection.sendRequest("sandbox.getHostSupport", {}), + /** @experimental */ + proxyCa: { + /** + * Reports whether the persistent certificate authority of the sandbox credential proxy exists, whether OS trust includes it, and whether it must be rotated. Changes nothing. + * + * @param params Identifies the credential hosts that the persistent certificate authority of the sandbox credential proxy must cover. The runtime always adds the hosts from the saved user settings. + * + * @returns Status of the persistent certificate authority of the sandbox credential proxy. + */ + getStatus: async (params: SandboxProxyCaRequest): Promise => + connection.sendRequest("sandbox.proxyCa.getStatus", params), + /** + * Creates the persistent certificate authority of the sandbox credential proxy if none is stored, without changing OS trust, and returns the path of its public certificate. Keeps an existing certificate authority, even one that must be rotated. Fails where OS trust is unsupported. Trust it with sandbox.proxyCa.trust: the CLI trusts only the hosts in the saved user settings, so it refuses a certificate authority that also covers hosts from sandboxConfig. + * + * @param params Identifies the credential hosts that the persistent certificate authority of the sandbox credential proxy must cover. The runtime always adds the hosts from the saved user settings. + * + * @returns Result of creating the persistent certificate authority of the sandbox credential proxy. + */ + create: async (params: SandboxProxyCaRequest): Promise => + connection.sendRequest("sandbox.proxyCa.create", params), + /** + * Replaces the persistent certificate authority of the sandbox credential proxy with a new one for the current credential hosts. If OS trust included the old one, removes it and trusts the new one, which can show an OS authentication prompt. Running sandboxed tools keep the old certificate authority until they restart. + * + * @param params Identifies the credential hosts that the persistent certificate authority of the sandbox credential proxy must cover. The runtime always adds the hosts from the saved user settings. + * + * @returns Status of the persistent certificate authority of the sandbox credential proxy. + */ + rotate: async (params: SandboxProxyCaRequest): Promise => + connection.sendRequest("sandbox.proxyCa.rotate", params), + /** + * Adds the persistent certificate authority of the sandbox credential proxy to OS trust, so sandboxed clients that read only OS trust accept the proxy. Call create first. Refuses a certificate authority that is not constrained to the current credential hosts. Can show an OS authentication prompt. + * + * @param params Identifies the credential hosts that the persistent certificate authority of the sandbox credential proxy must cover. The runtime always adds the hosts from the saved user settings. + * + * @returns Status of the persistent certificate authority of the sandbox credential proxy. + */ + trust: async (params: SandboxProxyCaRequest): Promise => + connection.sendRequest("sandbox.proxyCa.trust", params), + /** + * Removes the persistent certificate authority of the sandbox credential proxy from OS trust. Keeps the stored certificate authority. Can show an OS authentication prompt. Sandboxed clients that read only OS trust then reject the proxy; clients that read the per-process certificate bundle continue to work. + * + * @returns Status of the persistent certificate authority of the sandbox credential proxy. + */ + remove: async (): Promise => + connection.sendRequest("sandbox.proxyCa.remove", {}), + }, }, /** @experimental */ tools: { @@ -31076,24 +31712,61 @@ export function createInternalServerRpc(connection: MessageConnection) { configureSessionExtensions: async (params: ConfigureSessionExtensionsParams): Promise => connection.sendRequest("sessions.configureSessionExtensions", params), }, - /** @experimental */ - accounts: { - /** - * Acquire a Microsoft Entra access token through the runtime's OneAuth broker. Account-scoped because it uses the same native broker as the account stack: a trusted host application mints a scoped Entra token for its own use, most notably to authenticate to a remote MCP server whose authorization server is Entra ID (in place of the generic browser-OAuth flow). - * - * @param params OneAuth token request supplied by a trusted host application. - * - * @returns Result of a OneAuth token acquisition. - */ - acquireEntraToken: async (params: EntraTokenAcquireRequest): Promise => - connection.sendRequest("accounts.acquireEntraToken", params), - }, }; } /** Create typed session-scoped RPC methods. */ export function createSessionRpc(connection: MessageConnection, sessionId: string) { return { + /** @experimental */ + providers: { + /** + * Returns adapter definitions and supported operations in this session's effective provider catalog, without running discovery. Does not list provider instances or select inference models. + * + * @returns Normalized model-provider adapter definitions available to the session, not discovered instances. + */ + getCatalog: async (): Promise => + connection.sendRequest("session.providers.getCatalog", { sessionId }), + /** + * Discovers reachable instances using an adapter from this session's effective provider catalog and provider-specific discovery input. + * + * @param params Provider discovery parameters. + * + * @returns Provider instances found by a discovery operation. + */ + discover: async (params: ModelProviderDiscoverRequest): Promise => + connection.sendRequest("session.providers.discover", { ...params, sessionId }), + /** + * Gets current health and version information for a discovered model-provider instance. + * + * @param params Provider status request parameters. + * + * @returns Current health information for a provider instance. + */ + getStatus: async (params: ModelProviderGetStatusRequest): Promise => + connection.sendRequest("session.providers.getStatus", { ...params, sessionId }), + /** @experimental */ + models: { + /** + * Lists models installed or otherwise available from a discovered model-provider instance. + * + * @param params Provider model inventory request parameters. + * + * @returns Models offered for agent conversations by one provider instance. Adapters exclude known-incompatible models, but retain candidates with unknown capabilities. Listing does not guarantee compatibility. + */ + list: async (params: ModelProviderModelsListRequest): Promise => + connection.sendRequest("session.providers.models.list", { ...params, sessionId }), + /** + * Translates a discovered model into the provider and model configuration needed to use it, and reports whether each is already registered in this session. Prepares only: it registers nothing, writes nothing, and performs no provider requests. + * + * @param params A discovered instance and one of its models to translate into provider configuration. Pass back the instance and model as returned by `session.providers.discover` and `session.providers.models.list`. + * + * @returns Provider configuration prepared from a discovered model. Preparing a plan changes nothing: it neither registers the model with the session nor writes durable configuration. To apply it, pass `provider` and `model` to `session.provider.add`, omitting whichever the dispositions report as already configured. + */ + prepareConfiguration: async (params: ModelProviderPrepareConfigurationRequest): Promise => + connection.sendRequest("session.providers.models.prepareConfiguration", { ...params, sessionId }), + }, + }, /** * Suspends the session while preserving persisted state for later resume. * @@ -31777,19 +32450,21 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin getSources: async (): Promise => connection.sendRequest("session.instructions.getSources", { sessionId }), /** - * Invalidates cached custom-instruction discovery so subsequent turns and source reads observe instruction files currently on disk. + * For local sessions, invalidates instruction discovery and the model-facing prompt, then returns freshly discovered sources. The updated prompt takes effect on the next turn. Remote sessions must reload on their agent host instead. + * + * @returns Instruction sources loaded for the session, in merge order. */ - reload: async (): Promise => + reload: async (): Promise => connection.sendRequest("session.instructions.reload", { sessionId }), }, /** @experimental */ customizations: { /** - * Reloads all repository and user customizations for the active session: instructions, plugins and their MCP servers and hooks, custom agents, extensions, and skills. Returns diagnostics from the final skill reload. + * For local sessions, reconciles repository context and discovered instructions, plugins, skills, agents, hooks, MCP servers, and extensions after files appear or change under the working directory. Independent component failures are returned in outcomes and errors; a rejected call can have partially applied earlier steps. Remote sessions must reload on their agent host instead. The model-facing context is rebuilt on the next turn. * - * @returns Diagnostics from reloading skill definitions, with warnings and errors as separate lists. + * @returns Results of reloading discovered session customizations. Inspect outcomes for reloaded, skipped, or failed subsystems; a rejection may follow partial mutation. Changes to the model-facing prompt and tools apply on the next turn. */ - reload: async (): Promise => + reload: async (): Promise => connection.sendRequest("session.customizations.reload", { sessionId }), }, /** @experimental */ @@ -33714,6 +34389,36 @@ export function createInternalSessionRpc(connection: MessageConnection, sessionI connection.sendRequest("session.commands.finalizeInvocationEffect", { ...params, sessionId }), }, /** @experimental */ + ui: { + /** + * Resolves a pending elicitation request after direct interaction in the trusted in-process client. Only an accepted response to the built-in ask_user tool can become trusted human evidence. + * + * @param params Pending elicitation request ID and the user's response (accept/decline/cancel + form values). + * + * @returns Indicates whether the elicitation response was accepted; false if it was already resolved by another client. + */ + handleHumanAskUser: async (params: UIHandlePendingElicitationRequest): Promise => + connection.sendRequest("session.ui.handleHumanAskUser", { ...params, sessionId }), + /** + * Resolves a pending `user_input.requested` event after direct interaction in the trusted in-process client. + * + * @param params Request ID of a pending `user_input.requested` event and the user's response. + * + * @returns Indicates whether the pending UI request was resolved by this call. + */ + handleHumanUserInput: async (params: UIHandlePendingUserInputRequest): Promise => + connection.sendRequest("session.ui.handleHumanUserInput", { ...params, sessionId }), + /** + * Resolves a pending `exit_plan_mode.requested` event after direct interaction in the trusted in-process client. + * + * @param params Request ID of a pending `exit_plan_mode.requested` event and the user's response. + * + * @returns Indicates whether the pending UI request was resolved by this call. + */ + handleHumanExitPlanMode: async (params: UIHandlePendingExitPlanModeRequest): Promise => + connection.sendRequest("session.ui.handleHumanExitPlanMode", { ...params, sessionId }), + }, + /** @experimental */ settings: { /** * Returns a redacted snapshot of session runtime settings, with secrets and raw feature flags excluded. Internal: the runtime settings shape is a runtime-internal surface and is deliberately kept out of the public SDK, because consumers should not depend on the runtime's internal settings layout. It remains callable in-process and is expected to be reworked as the runtime internals are consolidated. @@ -33916,6 +34621,14 @@ export interface SessionFsHandler { * @returns File content as a UTF-8 string, or a filesystem error if the read failed. */ readFile(params: SessionFsReadFileRequest): Promise; + /** + * Reads binary file content from the client-provided session filesystem. + * + * @param params Path of the binary file to read from the client-provided session filesystem. + * + * @returns File bytes as standard base64, or a filesystem error if the read failed. + */ + readFileBytes(params: SessionFsReadFileBytesRequest): Promise; /** * Writes a file in the client-provided session filesystem. * @@ -33924,6 +34637,14 @@ export interface SessionFsHandler { * @returns Describes a filesystem error. */ writeFile(params: SessionFsWriteFileRequest): Promise; + /** + * Writes binary file content to the client-provided session filesystem. + * + * @param params File path, standard-base64-encoded bytes to write, and optional mode for the client-provided session filesystem. + * + * @returns Describes a filesystem error. + */ + writeFileBytes(params: SessionFsWriteFileBytesRequest): Promise; /** * Appends content to a file in the client-provided session filesystem, creating parent directories as needed. * @@ -34085,11 +34806,21 @@ export function registerClientSessionApiHandlers( if (!handler) throw new Error(`No sessionFs handler registered for session: ${params.sessionId}`); return handler.readFile(params); }); + connection.onRequest("sessionFs.readFileBytes", async (params: SessionFsReadFileBytesRequest) => { + const handler = getHandlers(params.sessionId).sessionFs; + if (!handler) throw new Error(`No sessionFs handler registered for session: ${params.sessionId}`); + return handler.readFileBytes(params); + }); connection.onRequest("sessionFs.writeFile", async (params: SessionFsWriteFileRequest) => { const handler = getHandlers(params.sessionId).sessionFs; if (!handler) throw new Error(`No sessionFs handler registered for session: ${params.sessionId}`); return handler.writeFile(params); }); + connection.onRequest("sessionFs.writeFileBytes", async (params: SessionFsWriteFileBytesRequest) => { + const handler = getHandlers(params.sessionId).sessionFs; + if (!handler) throw new Error(`No sessionFs handler registered for session: ${params.sessionId}`); + return handler.writeFileBytes(params); + }); connection.onRequest("sessionFs.appendFile", async (params: SessionFsAppendFileRequest) => { const handler = getHandlers(params.sessionId).sessionFs; if (!handler) throw new Error(`No sessionFs handler registered for session: ${params.sessionId}`); diff --git a/nodejs/src/generated/session-events.ts b/nodejs/src/generated/session-events.ts index 8f30f0b1fc..d6e4a099d9 100644 --- a/nodejs/src/generated/session-events.ts +++ b/nodejs/src/generated/session-events.ts @@ -76,6 +76,7 @@ export type SessionEvent = | ToolUserRequestedEvent | ToolExecutionStartEvent | ToolExecutionPartialResultEvent + | ToolShellOutputEvent | ToolExecutionProgressEvent | ToolExecutionCompleteEvent | ToolSearchActivatedEvent @@ -128,6 +129,7 @@ export type SessionEvent = | CapabilitiesChangedEvent | ExitPlanModeRequestedEvent | ExitPlanModeCompletedEvent + | HumanResponseRecordedEvent | ToolsUpdatedEvent | BackgroundTasksChangedEvent | WorkflowRunUpdatedEvent @@ -945,6 +947,28 @@ export type ToolExecutionStartToolDescriptionMetaUIVisibility = | "model" /** Tool is callable by the MCP App view (iframe) via session.mcp.apps.callTool */ | "app"; +/** + * Shell output source. Terminal output has no separate stdout/stderr attribution. + */ +export type ToolShellOutputStream = + /** Output from the shell command's standard output stream. This is the default when stream is omitted. */ + | "stdout" + /** Output from the shell command's standard error stream. */ + | "stderr" + /** Inherently merged output that cannot be attributed separately to stdout or stderr. */ + | "terminal"; +/** + * Kind of file mutation committed by a built-in editing tool. + */ +/** @experimental */ +export type ToolExecutionCompleteFileEditKind = + /** A file was created. */ + | "create" + /** A file was written by an edit operation. */ + | "edit" + /** A file was deleted. */ + | "delete" + | (string & {}); /** * A model-facing binary result as persisted: full inline data, a size-omitted marker, or a deduplicated asset reference */ @@ -1449,6 +1473,95 @@ export type ExitPlanModeAction = | "autopilot" /** Exit plan mode and continue with parallel autonomous workers. */ | "autopilot_fleet"; +/** + * Controlled provenance for a typed runtime response. Only `human_response`, minted by a trusted direct-interaction ingress, is human authorization evidence. + */ +export type HumanResponseActor = + /** A built-in trusted client submitted the response after direct human interaction. */ + | "human_response" + /** A host or SDK automation submitted the response without direct human interaction. */ + | "host_automation" + /** The response came through a legacy or otherwise unattributed ingress. */ + | "unknown"; +/** + * Exact runtime-owned question or reviewed plan paired with the typed response that settled it. + */ +export type HumanResponseRecordedResponse = + | { + /** + * Exact answer content accepted from the user. + */ + content: { + [k: string]: ElicitationCompletedContent | undefined; + }; + /** + * Exact question displayed to the user. + */ + message: string; + requestedSchema: ElicitationRequestedSchema; + /** + * Runtime-owned response kind discriminator. + */ + responseKind: "ask_user"; + } + | { + /** + * Whether the displayed request allowed a free-form answer. + */ + allowFreeform?: boolean; + /** + * Exact selected or free-form answer submitted by the user. + */ + answer: string; + /** + * Exact choices displayed to the user, when the request offered choices. + */ + choices?: string[]; + /** + * Exact question displayed to the user. + */ + question: string; + /** + * Runtime-owned response kind discriminator. + */ + responseKind: "user_input"; + /** + * Whether the answer was typed as free-form text rather than selected from the displayed choices. + */ + wasFreeform: boolean; + } + | { + /** + * Actions offered by the plan review UI. + */ + actions: ExitPlanModeAction[]; + /** + * Whether the user approved the reviewed plan. + */ + approved: boolean; + /** + * Whether the selected response requested edit auto-approval. + */ + autoApproveEdits?: boolean; + /** + * Exact feedback submitted with the plan decision, when present. + */ + feedback?: string; + /** + * Exact full plan content available from the review UI. + */ + planContent: string; + recommendedAction: ExitPlanModeAction; + /** + * Runtime-owned response kind discriminator. + */ + responseKind: "exit_plan_mode"; + selectedAction?: ExitPlanModeAction; + /** + * Exact plan summary displayed to the user. + */ + summary: string; + }; /** * Terminal status a workflow run committed. A settled run is never `pending` or `running`, so those two members of the run-status domain are deliberately absent. */ @@ -1595,6 +1708,10 @@ export interface StartData { * Reasoning effort level used for model calls, if applicable (e.g. "none", "low", "medium", "high", "xhigh", "max") */ reasoningEffort?: string; + /** + * Model that owns effort embedded in an authored model selection. Omitted for independent reasoning-effort overrides and legacy events. + */ + reasoningEffortModel?: string; reasoningSummary?: ReasoningSummary; /** * Whether this session supports remote steering via GitHub @@ -1747,6 +1864,10 @@ export interface ResumeData { * Reasoning effort level used for model calls, if applicable (e.g. "none", "low", "medium", "high", "xhigh", "max") */ reasoningEffort?: string; + /** + * Model that owns effort embedded in an authored model selection. Omitted for independent reasoning-effort overrides and legacy events. + */ + reasoningEffortModel?: string; reasoningSummary?: ReasoningSummary; /** * Whether this session supports remote steering via GitHub @@ -2349,6 +2470,10 @@ export interface ModelChangeData { * Reasoning effort level after the model change, if applicable */ reasoningEffort?: string | null; + /** + * Model that owns effort embedded in an authored model selection. Omitted for independent reasoning-effort overrides and legacy events. + */ + reasoningEffortModel?: string; reasoningSummary?: ReasoningSummary; source?: ModelChangeSource; verbosity?: Verbosity; @@ -5853,6 +5978,10 @@ export interface AssistantMessageReasoningBlocks { * Provider-native reasoning items or content blocks preserved verbatim, in order. A single response can carry several, and provider signatures or identifiers may depend on their exact content and ordering. */ blocks?: JsonValue[]; + /** + * Anthropic Messages assistant block ordering preserved when the legacy reasoning-only representation cannot reproduce it exactly. Thinking and text blocks remain verbatim; tool-use entries retain identity and a payload fingerprint when later signed reasoning depends on them, and are hydrated from the message's tool requests during replay. + */ + orderedBlocks?: JsonValue[]; /** * Model provider that produced these reasoning blocks. */ @@ -6910,7 +7039,8 @@ export interface ToolExecutionStartToolDescriptionMetaUI { visibility?: ToolExecutionStartToolDescriptionMetaUIVisibility[]; } /** - * Session event "tool.execution_partial_result". Streaming tool execution output for incremental result display + * @deprecated + * Session event "tool.execution_partial_result". Deprecated merged replacement snapshot of shell output. Use tool.shell_output for append-only, stream-tagged output instead. */ export interface ToolExecutionPartialResultEvent { /** @@ -6940,11 +7070,12 @@ export interface ToolExecutionPartialResultEvent { type: "tool.execution_partial_result"; } /** - * Streaming tool execution output for incremental result display + * @deprecated + * Deprecated merged replacement snapshot of shell output. Use tool.shell_output for append-only, stream-tagged output instead. */ export interface ToolExecutionPartialData { /** - * Incremental output chunk from the running tool + * Merged replacement snapshot from the running shell, not an append-only chunk */ partialOutput: string; /** @@ -6952,6 +7083,54 @@ export interface ToolExecutionPartialData { */ toolCallId: string; } +/** + * Session event "tool.shell_output". Live, append-only shell output. Not persisted or replayed to late subscribers. Text is decoded and redacted per chunk; chunks need not contain complete lines. + */ +export interface ToolShellOutputEvent { + /** + * Sub-agent instance identifier. Absent for events from the root/main agent and session-level events. + */ + agentId?: string; + data: ToolShellOutputData; + /** + * Always true for events that are transient and not persisted to the session event log on disk. + */ + ephemeral: true; + /** + * Unique event identifier (UUID v4), generated when the event is emitted + */ + id: string; + /** + * ID of the chronologically preceding event in the session, forming a linked chain. Null for the first event. + */ + parentId: string | null; + /** + * ISO 8601 timestamp when the event was created + */ + timestamp: string; + /** + * Type discriminator. Always "tool.shell_output". + */ + type: "tool.shell_output"; +} +/** + * Live, append-only shell output. Not persisted or replayed to late subscribers. Text is decoded and redacted per chunk; chunks need not contain complete lines. + */ +export interface ToolShellOutputData { + /** + * Zero-based publication sequence across all output streams for this tool call. Not a byte offset or an OS write-order guarantee. + */ + sequence: number; + stream?: ToolShellOutputStream; + /** + * New output to append, without synthetic shell-result markers or stream-switch separators + */ + text: string; + /** + * Tool call ID that owns this shell output + */ + toolCallId: string; +} /** * Session event "tool.execution_progress". Tool execution progress notification with status message */ @@ -7036,6 +7215,12 @@ export interface ToolExecutionCompleteEvent { */ export interface ToolExecutionCompleteData { error?: ToolExecutionCompleteError; + /** + * Experimental. File mutations actually committed by a built-in file editing tool, in execution order. Present on successful edits and on partial failures when earlier mutations were committed. Paths are absolute in the session filesystem namespace. + * + * @experimental + */ + fileEdits?: ToolExecutionCompleteFileEdit[]; /** * Experimental HydraFusion attribution for this tool completion. * @@ -7114,6 +7299,17 @@ export interface ToolExecutionCompleteError { message: string; remediation?: RemediationAction; } +/** + * A file mutation that was actually committed by a built-in file editing tool. + */ +/** @experimental */ +export interface ToolExecutionCompleteFileEdit { + kind: ToolExecutionCompleteFileEditKind; + /** + * Absolute path in the session filesystem namespace. + */ + path: string; +} /** * Tool execution result on success */ @@ -12174,6 +12370,51 @@ export interface ExitPlanModeCompletedData { requestId: string; selectedAction?: ExitPlanModeAction; } +/** + * Session event "human_response.recorded". Durable request-correlated evidence for a typed response to a runtime-owned question or plan review. + */ +export interface HumanResponseRecordedEvent { + /** + * Sub-agent instance identifier. Absent for events from the root/main agent and session-level events. + */ + agentId?: string; + data: HumanResponseRecordedData; + /** + * When true, the event is transient and not persisted to the session event log on disk + */ + ephemeral?: boolean; + /** + * Unique event identifier (UUID v4), generated when the event is emitted + */ + id: string; + /** + * ID of the chronologically preceding event in the session, forming a linked chain. Null for the first event. + */ + parentId: string | null; + /** + * ISO 8601 timestamp when the event was created + */ + timestamp: string; + /** + * Type discriminator. Always "human_response.recorded". + */ + type: "human_response.recorded"; +} +/** + * Durable request-correlated evidence for a typed response to a runtime-owned question or plan review. + */ +export interface HumanResponseRecordedData { + actor: HumanResponseActor; + /** + * Request ID of the runtime-owned question or plan review. + */ + requestId: string; + response: HumanResponseRecordedResponse; + /** + * Tool call ID that opened the request, when present. + */ + toolCallId?: string; +} /** * Session event "session.tools_updated". Payload of `session.tools_updated` identifying the model whose resolved tools were updated. */ diff --git a/nodejs/src/index.ts b/nodejs/src/index.ts index c38b58a5ec..897be6cb1c 100644 --- a/nodejs/src/index.ts +++ b/nodejs/src/index.ts @@ -43,6 +43,7 @@ export { CopilotWebSocketCloseStatus, CopilotWebSocketForwarder, SessionFsSqliteTransactionFailure, + SessionFsWriteFailure, SYSTEM_MESSAGE_SECTIONS, } from "./types.js"; // Re-export the generated session-event types (every *Event interface and @@ -92,6 +93,12 @@ export type { AgentStopHandler, AgentStopHookInput, AgentStopHookOutput, + SubagentStartHandler, + SubagentStartHookInput, + SubagentStartHookOutput, + SubagentStopHandler, + SubagentStopHookInput, + SubagentStopHookOutput, UserPromptTransformedHandler, UserPromptTransformedHookInput, UserPromptTransformedHookOutput, diff --git a/nodejs/src/runtimeArtifacts.ts b/nodejs/src/runtimeArtifacts.ts index d33b72c9da..9f749486ac 100644 --- a/nodejs/src/runtimeArtifacts.ts +++ b/nodejs/src/runtimeArtifacts.ts @@ -43,6 +43,7 @@ const EXCLUDED_TOP_LEVEL = new Set([ "app.js", "assets", "changelog.json", + "cli-main.js", "copilot", "copilot.exe", "foundry-local-sdk", diff --git a/nodejs/src/session.ts b/nodejs/src/session.ts index 825464407f..5790ff2643 100644 --- a/nodejs/src/session.ts +++ b/nodejs/src/session.ts @@ -1554,7 +1554,29 @@ export class CopilotSession { for (const tool of tools) { if (tool.handler) { - this.toolHandlers.set(tool.name, tool.handler); + const handler = tool.handler; + if ( + tool.name === "apply_patch" && + tool.overridesBuiltInTool && + toJsonSchema(tool.parameters)?.type === "string" + ) { + this.toolHandlers.set(tool.name, (args, invocation) => { + if (typeof args === "string") { + return handler(args, invocation); + } + if ( + typeof args === "object" && + args !== null && + "input" in args && + typeof args.input === "string" + ) { + return handler(args.input, invocation); + } + throw new TypeError("apply_patch string override requires a string input"); + }); + } else { + this.toolHandlers.set(tool.name, handler); + } } } } @@ -2228,6 +2250,8 @@ export class CopilotSession { sessionEnd: this.hooks.onSessionEnd as GenericHandler | undefined, errorOccurred: this.hooks.onErrorOccurred as GenericHandler | undefined, agentStop: this.hooks.onAgentStop as GenericHandler | undefined, + subagentStart: this.hooks.onSubagentStart as GenericHandler | undefined, + subagentStop: this.hooks.onSubagentStop as GenericHandler | undefined, }; const handler = handlerMap[hookType]; diff --git a/nodejs/src/sessionFsProvider.ts b/nodejs/src/sessionFsProvider.ts index ecb18a5702..ee9908682d 100644 --- a/nodejs/src/sessionFsProvider.ts +++ b/nodejs/src/sessionFsProvider.ts @@ -13,6 +13,9 @@ import type { SessionFsSqliteQueryType, } from "./generated/rpc.js"; +const MAX_BINARY_BYTES = ((64 * 1024 * 1024 - 1024) / 4) * 3; +const MAX_BINARY_CONTENT_LENGTH = Math.ceil(MAX_BINARY_BYTES / 3) * 4; + export type { SessionFsSqliteQueryType, SessionFsSqliteTransactionErrorClass }; /** @@ -63,6 +66,11 @@ export class SessionFsSqliteTransactionFailure extends Error { } } +/** Throw from `writeFile` only when the provider changed the target before failing. */ +export class SessionFsWriteFailure extends Error { + readonly writeChanged = true; +} + /** * SQLite operations for the per-session database. * Implementers provide query execution and existence checking. @@ -112,7 +120,13 @@ export interface SessionFsProvider { /** Reads the full content of a file. Throw if the file does not exist. */ readFile(path: string): Promise; - /** Writes content to a file, creating parent directories if needed. */ + /** Read exact file bytes. Required when capabilities.binary is enabled. */ + readFileBytes?(path: string): Promise; + + /** Write exact file bytes. Required when capabilities.binary is enabled. */ + writeFileBytes?(path: string, content: Uint8Array, mode?: number): Promise; + + /** Writes content to a file, creating parent directories if needed. Throw {@link SessionFsWriteFailure} if a failed write changed the target. */ writeFile(path: string, content: string, mode?: number): Promise; /** Appends content to a file, creating parent directories if needed. */ @@ -174,10 +188,63 @@ export function createSessionFsAdapter(provider: SessionFsProvider): SessionFsHa return { content: "", error: toSessionFsError(err) }; } }, + readFileBytes: async ({ path }) => { + if (!provider.readFileBytes) { + return { + content: "", + error: { code: "UNKNOWN", message: "Binary reads are not supported" }, + }; + } + try { + const bytes = await provider.readFileBytes(path); + if (bytes.length > MAX_BINARY_BYTES) { + return { + content: "", + error: { + code: "UNKNOWN", + message: + "sessionFs.readFileBytes content exceeds the binary read limit", + }, + }; + } + return { + content: Buffer.from(bytes).toString("base64"), + }; + } catch (err) { + return { content: "", error: toSessionFsError(err) }; + } + }, writeFile: async ({ path, content, mode }) => { try { await provider.writeFile(path, content, mode); return undefined; + } catch (err) { + const error = toSessionFsError(err); + return err instanceof SessionFsWriteFailure + ? { ...error, writeChanged: true } + : error; + } + }, + writeFileBytes: async ({ path, content, mode }) => { + if (!provider.writeFileBytes) { + return { code: "UNKNOWN", message: "Binary writes are not supported" }; + } + if (content.length > MAX_BINARY_CONTENT_LENGTH) { + return { + code: "UNKNOWN", + message: "sessionFs.writeFileBytes content exceeds the binary write limit", + }; + } + const bytes = Buffer.from(content, "base64"); + if (bytes.toString("base64") !== content || bytes.length > MAX_BINARY_BYTES) { + return { + code: "UNKNOWN", + message: "invalid sessionFs.writeFileBytes base64 content", + }; + } + try { + await provider.writeFileBytes(path, bytes, mode); + return undefined; } catch (err) { return toSessionFsError(err); } diff --git a/nodejs/src/types.ts b/nodejs/src/types.ts index 73ad7e4f87..7b0f1fcf8e 100644 --- a/nodejs/src/types.ts +++ b/nodejs/src/types.ts @@ -120,6 +120,7 @@ export type { SessionFsSqliteProvider } from "./sessionFsProvider.js"; export type { SessionFsSqliteStatement } from "./sessionFsProvider.js"; export type { SessionFsSqliteTransactionErrorClass } from "./sessionFsProvider.js"; export { SessionFsSqliteTransactionFailure } from "./sessionFsProvider.js"; +export { SessionFsWriteFailure } from "./sessionFsProvider.js"; export type { LlmInferenceHeaders } from "./generated/rpc.js"; export type { PermissionDecisionContext, @@ -1156,11 +1157,11 @@ export const SYSTEM_MESSAGE_SECTIONS: Record Promise | AgentStopHookOutput | void; +/** + * Input for the hook fired before a sub-agent's first turn. + * + * The session metadata belongs to the parent session, not the child. + */ +export interface SubagentStartHookInput extends BaseHookInput { + transcriptPath: string; + agentName: string; + agentDisplayName?: string; + agentDescription?: string; +} + +/** Output for the sub-agent start hook. Context is prepended to the child's initial prompt. */ +export interface SubagentStartHookOutput { + additionalContext?: string; +} + +/** Handler for the sub-agent start hook. */ +export type SubagentStartHandler = ( + input: SubagentStartHookInput, + invocation: { sessionId: string } +) => Promise | SubagentStartHookOutput | void; + +/** + * Input for the hook fired after a sub-agent completes a turn. + * + * The response is the child's last assistant message before any hook rewrite. + */ +export interface SubagentStopHookInput extends SubagentStartHookInput { + agentId?: string; + agentType: string; + stopReason: "end_turn"; + response: string; +} + +/** + * Output for the sub-agent stop hook. `"block"` with a nonempty `reason` continues + * the child; otherwise `modifiedResponse` replaces the response reported to the parent. + * When both are supplied, a valid block takes precedence over the rewrite. + */ +export type SubagentStopHookOutput = + | { decision: "block"; reason: string; modifiedResponse?: string } + | { decision?: "allow"; reason?: never; modifiedResponse?: string }; + +/** Handler for the sub-agent stop hook. */ +export type SubagentStopHandler = ( + input: SubagentStopHookInput, + invocation: { sessionId: string } +) => Promise | SubagentStopHookOutput | void; + /** * Configuration for session hooks */ @@ -1855,6 +1909,15 @@ export interface SessionHooks { * agent stop. */ onAgentStop?: AgentStopHandler; + + /** Called before a sub-agent's first turn. Return context to prepend to its prompt. */ + onSubagentStart?: SubagentStartHandler; + + /** + * Called after a sub-agent completes a turn. Return a block reason to + * continue the child, or a replacement response to report to the parent. + */ + onSubagentStop?: SubagentStopHandler; } // ============================================================================ @@ -3546,6 +3609,12 @@ export interface SessionFsConfig { * @default false */ sqlite?: boolean; + /** + * Whether this provider supports exact binary reads and writes through readFileBytes and writeFileBytes. + * Required to view images stored only in the provider. + * @default false + */ + binary?: boolean; }; } diff --git a/nodejs/test/client.test.ts b/nodejs/test/client.test.ts index c1e577fefc..d0466fb7a2 100644 --- a/nodejs/test/client.test.ts +++ b/nodejs/test/client.test.ts @@ -25,6 +25,9 @@ import { type GitHubTelemetryNotification, type ManagedSettings, type ModelInfo, + type SubagentStartHookOutput, + type SubagentStopHookOutput, + type SessionFsProvider, } from "../src/index.js"; import { CopilotSession } from "../src/session.js"; import { defaultJoinSessionPermissionHandler } from "../src/types.js"; @@ -1839,6 +1842,122 @@ describe("CopilotClient", () => { ); }); + it("deletes an uninitialized cloud session after binary provider validation fails", async () => { + const client = new CopilotClient({ + sessionFs: { + initialCwd: "/", + sessionStatePath: "/state", + conventions: "posix", + capabilities: { binary: true }, + }, + }); + await client.start(); + onTestFinished(() => stopClient(client)); + + const spy = vi + .spyOn((client as any).connection!, "sendRequest") + .mockImplementation(async (method: string) => { + if (method === "session.create") return { sessionId: "cloud-invalid-provider" }; + if (method === "session.delete") return { success: true }; + throw new Error(`Unexpected method: ${method}`); + }); + try { + await expect( + client.createSession({ + onPermissionRequest: approveAll, + cloud: { repository: { owner: "github", name: "copilot-sdk", branch: "main" } }, + createSessionFsProvider: () => ({}) as SessionFsProvider, + }) + ).rejects.toThrow("does not implement readFileBytes"); + expect(spy).toHaveBeenCalledWith("session.delete", { + sessionId: "cloud-invalid-provider", + }); + expect((client as any).sessions.has("cloud-invalid-provider")).toBe(false); + } finally { + spy.mockRestore(); + } + }); + + it("bounds cloud session cleanup when binary provider validation fails", async () => { + const client = new CopilotClient({ + sessionFs: { + initialCwd: "/", + sessionStatePath: "/state", + conventions: "posix", + capabilities: { binary: true }, + }, + }); + await client.start(); + onTestFinished(() => stopClient(client)); + + const deleteStarted = Promise.withResolvers(); + const spy = vi + .spyOn((client as any).connection!, "sendRequest") + .mockImplementation(async (method: string) => { + if (method === "session.create") return { sessionId: "cloud-stalled-delete" }; + if (method === "session.delete") { + deleteStarted.resolve(); + return new Promise(() => {}); + } + throw new Error(`Unexpected method: ${method}`); + }); + vi.useFakeTimers({ toFake: ["setTimeout", "clearTimeout"] }); + try { + const failure = client + .createSession({ + onPermissionRequest: approveAll, + cloud: { repository: { owner: "github", name: "copilot-sdk", branch: "main" } }, + createSessionFsProvider: () => ({}) as SessionFsProvider, + }) + .catch((error: unknown) => error); + + await deleteStarted.promise; + await vi.advanceTimersByTimeAsync(10_000); + + const error = await failure; + expect(error).toBeInstanceOf(AggregateError); + if (!(error instanceof AggregateError)) throw error; + expect(error.errors).toEqual([ + expect.objectContaining({ + message: expect.stringContaining("does not implement readFileBytes"), + }), + expect.objectContaining({ message: "session.delete timed out after 10000ms" }), + ]); + expect((client as any).sessions.has("cloud-stalled-delete")).toBe(false); + } finally { + vi.useRealTimers(); + spy.mockRestore(); + } + }); + + it("does not register a session when binary provider validation fails during local create or resume", async () => { + const client = new CopilotClient({ + sessionFs: { + initialCwd: "/", + sessionStatePath: "/state", + conventions: "posix", + capabilities: { binary: true }, + }, + }); + await client.start(); + onTestFinished(() => stopClient(client)); + + const invalidProvider = () => ({}) as SessionFsProvider; + await expect( + client.createSession({ + onPermissionRequest: approveAll, + createSessionFsProvider: invalidProvider, + }) + ).rejects.toThrow("does not implement readFileBytes"); + await expect( + client.resumeSession("missing-provider", { + onPermissionRequest: approveAll, + createSessionFsProvider: invalidProvider, + }) + ).rejects.toThrow("does not implement readFileBytes"); + expect((client as any).sessions.size).toBe(0); + }); + it("forwards clientName in session.resume request", async () => { const client = new CopilotClient(); await client.start(); @@ -4638,6 +4757,144 @@ connection.listen(); // No decision returned — the SDK forwards an empty output envelope. expect(response).toEqual({ output: undefined }); }); + + it("routes subagentStart hooks.invoke with normalized parent metadata and additional context", async () => { + const client = new CopilotClient({ autoStart: false }); + + const received: { input: unknown; invocation: { sessionId: string } }[] = []; + const session = new CopilotSession("session-1", {} as any); + session.registerHooks({ + onSubagentStart: async (input, invocation) => { + received.push({ input, invocation }); + return { + additionalContext: "Read the requested file.", + } satisfies SubagentStartHookOutput; + }, + }); + (client as any).sessions.set(session.sessionId, session); + + const response = await (client as any).handleHooksInvoke({ + sessionId: session.sessionId, + hookType: "subagentStart", + input: { + sessionId: "parent-runtime-id", + timestamp: 1700000000000, + cwd: "/repo", + transcriptPath: "/repo/transcript.jsonl", + agentName: "explore", + agentDisplayName: "Explore Agent", + agentDescription: "Reads files", + }, + }); + + expect(received).toEqual([ + { + input: { + sessionId: "parent-runtime-id", + timestamp: new Date(1700000000000), + workingDirectory: "/repo", + transcriptPath: "/repo/transcript.jsonl", + agentName: "explore", + agentDisplayName: "Explore Agent", + agentDescription: "Reads files", + }, + invocation: { sessionId: session.sessionId }, + }, + ]); + expect(response).toEqual({ output: { additionalContext: "Read the requested file." } }); + }); + + it("routes subagentStop hooks.invoke with its response and forwards an explicit allow and rewrite", async () => { + const client = new CopilotClient({ autoStart: false }); + + const received: { input: unknown; invocation: { sessionId: string } }[] = []; + const session = new CopilotSession("session-1", {} as any); + session.registerHooks({ + onSubagentStop: (input, invocation) => { + received.push({ input, invocation }); + return { + decision: "allow", + modifiedResponse: "Reviewed: " + input.response, + } satisfies SubagentStopHookOutput; + }, + }); + (client as any).sessions.set(session.sessionId, session); + + const response = await (client as any).handleHooksInvoke({ + sessionId: session.sessionId, + hookType: "subagentStop", + input: { + sessionId: "parent-runtime-id", + timestamp: 1700000000000, + cwd: "/repo", + transcriptPath: "/repo/transcript.jsonl", + agentId: "read-file", + agentType: "explore", + agentName: "explore", + stopReason: "end_turn", + response: "The file contains a greeting.", + }, + }); + + expect(received).toEqual([ + { + input: { + sessionId: "parent-runtime-id", + timestamp: new Date(1700000000000), + workingDirectory: "/repo", + transcriptPath: "/repo/transcript.jsonl", + agentId: "read-file", + agentType: "explore", + agentName: "explore", + stopReason: "end_turn", + response: "The file contains a greeting.", + }, + invocation: { sessionId: session.sessionId }, + }, + ]); + expect(response).toEqual({ + output: { + decision: "allow", + modifiedResponse: "Reviewed: The file contains a greeting.", + }, + }); + }); + + it("forwards a subagentStop block reason without invoking the top-level agentStop hook", async () => { + const client = new CopilotClient({ autoStart: false }); + + const topLevelStop = vi.fn(); + const session = new CopilotSession("session-1", {} as any); + session.registerHooks({ + onAgentStop: topLevelStop, + onSubagentStop: () => + ({ + decision: "block", + reason: "Read the rest of the file.", + }) satisfies SubagentStopHookOutput, + }); + (client as any).sessions.set(session.sessionId, session); + + const response = await (client as any).handleHooksInvoke({ + sessionId: session.sessionId, + hookType: "subagentStop", + input: { + sessionId: session.sessionId, + timestamp: 1700000000000, + cwd: "/repo", + transcriptPath: "/repo/transcript.jsonl", + agentType: "explore", + agentName: "explore", + stopReason: "end_turn", + response: "First line", + }, + }); + + expect(response).toEqual({ + output: { decision: "block", reason: "Read the rest of the file." }, + }); + expect(topLevelStop).not.toHaveBeenCalled(); + }); }); describe("shutdown", () => { diff --git a/nodejs/test/csharp-codegen.test.ts b/nodejs/test/csharp-codegen.test.ts index cc1f32cad3..1ad071369a 100644 --- a/nodejs/test/csharp-codegen.test.ts +++ b/nodejs/test/csharp-codegen.test.ts @@ -130,6 +130,61 @@ describe("C# additive request entry compatibility", () => { }); }); +describe("C# session event deprecation", () => { + it.each([ + { eventDeprecated: true, dataDeprecated: false }, + { eventDeprecated: false, dataDeprecated: true }, + { eventDeprecated: true, dataDeprecated: true }, + { eventDeprecated: false, dataDeprecated: false }, + ])( + "preserves target-compatible event=$eventDeprecated and payload=$dataDeprecated annotations", + ({ eventDeprecated, dataDeprecated }) => { + const code = generateSessionEventsCode({ + definitions: { + Payload: { + type: "object", + properties: { toolCallId: { type: "string" } }, + ...(dataDeprecated ? { deprecated: true } : {}), + }, + SessionEvent: { + anyOf: [ + { + type: "object", + ...(eventDeprecated ? { deprecated: true } : {}), + properties: { + type: { const: "tool.execution_partial_result" }, + data: { $ref: "#/definitions/Payload" }, + }, + required: ["type", "data"], + }, + ], + }, + }, + }); + const attributes = [ + "[EditorBrowsable(EditorBrowsableState.Never)]", + "#if NET5_0_OR_GREATER", + '[Obsolete("This member is deprecated and will be removed in a future version.", DiagnosticId = "GHCP001")]', + "#else", + '[Obsolete("This member is deprecated and will be removed in a future version.")]', + "#endif", + ].join("\n"); + + for (const [name, deprecated] of [ + ["ToolExecutionPartialResultEvent", eventDeprecated], + ["ToolExecutionPartialResultData", dataDeprecated], + ] as const) { + const declaration = `${attributes}\npublic sealed partial class ${name}`; + if (deprecated) expect(code).toContain(declaration); + else expect(code).not.toContain(declaration); + } + expect(code.match(/\[Obsolete\(/g) ?? []).toHaveLength( + 2 * (Number(eventDeprecated) + Number(dataDeprecated)) + ); + } + ); +}); + describe("C# root event payload unions", () => { it.each(["anyOf", "oneOf"] as const)("preserves referenced %s payload variants", (keyword) => { const code = generateSessionEventsCode({ diff --git a/nodejs/test/e2e/anthropic_replay.e2e.test.ts b/nodejs/test/e2e/anthropic_replay.e2e.test.ts new file mode 100644 index 0000000000..3b2bad6f2b --- /dev/null +++ b/nodejs/test/e2e/anthropic_replay.e2e.test.ts @@ -0,0 +1,206 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +import { describe, expect, it } from "vitest"; +import { approveAll, CopilotRequestHandler, type CopilotRequestContext } from "../../src/index.js"; +import { createSdkTestContext } from "./harness/sdkTestContext.js"; + +const MODEL = "claude-sonnet-5"; +const FIRST_PROMPT = "Use the sql tool to insert the Anthropic replay todo."; +const FOLLOWUP_PROMPT = "Confirm the Anthropic replay todo was inserted."; +const TOOL_ID = "toolu_sdk_replay"; +const TOOL_INPUT = { + description: "Insert Anthropic replay todo", + query: "INSERT INTO todos (id, title, status) VALUES ('sdk-replay', 'Anthropic replay', 'done')", +}; + +type Block = + | { type: "thinking"; thinking: string; signature: string } + | { type: "text"; text: string } + | { type: "tool_use"; id: string; name: string; input: typeof TOOL_INPUT }; + +function messageStream(blocks: Block[], stopReason: "tool_use" | "end_turn"): Response { + const events: Array<[string, unknown]> = [ + [ + "message_start", + { + type: "message_start", + message: { + id: "msg_sdk_replay", + type: "message", + role: "assistant", + model: MODEL, + content: [], + stop_reason: null, + stop_sequence: null, + usage: { input_tokens: 5, output_tokens: 1 }, + }, + }, + ], + ]; + for (const [index, block] of blocks.entries()) { + events.push([ + "content_block_start", + { + type: "content_block_start", + index, + content_block: + block.type === "tool_use" + ? { type: "tool_use", id: block.id, name: block.name, input: {} } + : { type: block.type, [block.type === "text" ? "text" : "thinking"]: "" }, + }, + ]); + events.push([ + "content_block_delta", + { + type: "content_block_delta", + index, + delta: + block.type === "thinking" + ? { type: "thinking_delta", thinking: block.thinking } + : block.type === "text" + ? { type: "text_delta", text: block.text } + : { type: "input_json_delta", partial_json: JSON.stringify(block.input) }, + }, + ]); + if (block.type === "thinking") { + events.push([ + "content_block_delta", + { + type: "content_block_delta", + index, + delta: { type: "signature_delta", signature: block.signature }, + }, + ]); + } + events.push(["content_block_stop", { type: "content_block_stop", index }]); + } + events.push( + [ + "message_delta", + { + type: "message_delta", + delta: { stop_reason: stopReason, stop_sequence: null }, + usage: { output_tokens: 10 }, + }, + ], + ["message_stop", { type: "message_stop" }] + ); + return new Response( + events + .map(([event, data]) => `event: ${event}\ndata: ${JSON.stringify(data)}\n\n`) + .join(""), + { status: 200, headers: { "content-type": "text/event-stream" } } + ); +} + +class AnthropicReplayHandler extends CopilotRequestHandler { + readonly requests: Array<{ messages: Array<{ role: string; content: unknown }> }> = []; + + constructor(private readonly blocks: Block[]) { + super(); + } + + protected override async sendRequest( + request: Request, + _ctx: CopilotRequestContext + ): Promise { + if (!request.url.endsWith("/messages")) { + throw new Error(`Unexpected SDK replay request: ${request.url}`); + } + const body = (await request.json()) as { + messages: Array<{ role: string; content: unknown }>; + }; + this.requests.push(body); + const messages = JSON.stringify(body.messages); + if (messages.includes(FOLLOWUP_PROMPT)) { + return messageStream([{ type: "text", text: "SDK_REPLAY_FOLLOWUP_DONE" }], "end_turn"); + } + if (messages.includes(TOOL_ID)) { + return messageStream([{ type: "text", text: "SDK_REPLAY_TOOL_DONE" }], "end_turn"); + } + return messageStream(this.blocks, "tool_use"); + } +} + +function replayedAssistantContent( + request: { messages: Array<{ role: string; content: unknown }> } | undefined +): unknown[] { + const assistant = request?.messages.find((message) => message.role === "assistant"); + if (!assistant || !Array.isArray(assistant.content)) { + throw new Error("Resumed Anthropic request has no assistant block array"); + } + return assistant.content; +} + +describe("Anthropic replay through SDK sessions", async () => { + const { createClient } = await createSdkTestContext(); + const thinkingOne = { + type: "thinking", + thinking: "First thought.", + signature: "sig-sdk-one", + } as const; + const thinkingTwo = { + type: "thinking", + thinking: "Second thought.", + signature: "sig-sdk-two", + } as const; + const tool = { type: "tool_use", id: TOOL_ID, name: "sql", input: TOOL_INPUT } as const; + + it.each([ + [ + "interleaved", + [ + thinkingOne, + { type: "text", text: "First narration." }, + thinkingTwo, + { type: "text", text: "Second narration." }, + tool, + ], + ], + ["adjacent legacy", [thinkingOne, thinkingTwo, tool]], + ] as const)("preserves %s thinking blocks across a cold SDK resume", async (_name, blocks) => { + const handler = new AnthropicReplayHandler([...blocks]); + const provider = { + type: "anthropic" as const, + baseUrl: "https://anthropic-replay.invalid/v1", + apiKey: "test-provider-key", + modelId: MODEL, + wireModel: MODEL, + }; + const firstClient = createClient({ requestHandler: handler }); + let sessionId: string; + try { + const session = await firstClient.createSession({ + onPermissionRequest: approveAll, + model: MODEL, + provider, + }); + sessionId = session.sessionId; + const result = await session.sendAndWait({ prompt: FIRST_PROMPT }); + expect(result?.data.content).toContain("SDK_REPLAY_TOOL_DONE"); + await session.disconnect(); + } finally { + await firstClient.stop(); + } + + const resumedClient = createClient({ requestHandler: handler }); + try { + const resumed = await resumedClient.resumeSession(sessionId, { + onPermissionRequest: approveAll, + model: MODEL, + provider, + }); + const result = await resumed.sendAndWait({ prompt: FOLLOWUP_PROMPT }); + expect(result?.data.content).toContain("SDK_REPLAY_FOLLOWUP_DONE"); + const followup = handler.requests.findLast((request) => + JSON.stringify(request.messages).includes(FOLLOWUP_PROMPT) + ); + expect(replayedAssistantContent(followup)).toEqual(blocks); + await resumed.disconnect(); + } finally { + await resumedClient.stop(); + } + }); +}); diff --git a/nodejs/test/e2e/auto_tier.e2e.test.ts b/nodejs/test/e2e/auto_tier.e2e.test.ts index f2b5717e60..21401be17d 100644 --- a/nodejs/test/e2e/auto_tier.e2e.test.ts +++ b/nodejs/test/e2e/auto_tier.e2e.test.ts @@ -6,6 +6,7 @@ import { describe, expect, it } from "vitest"; import { approveAll } from "../../src/index.js"; import { createSdkTestContext } from "./harness/sdkTestContext.js"; import { getNextEventOfType } from "./harness/sdkTestHelper.js"; +import { isByokBackend } from "./harness/testBackend"; /** * The runtime stages an Auto routing preference instead of applying it immediately: a @@ -129,7 +130,9 @@ describe("Auto tier switching", async () => { await overrideClient.stop(); }, 120_000); - it("should commit fast auto tier after successful turn", async () => { + // Activating Auto tiers requires CAPI routing, not a fixed BYOK provider. + it("should commit fast auto tier after successful turn", async ({ skip }) => { + skip(isByokBackend); const session = await client.createSession({ onPermissionRequest: approveAll, model: "auto", @@ -164,7 +167,8 @@ describe("Auto tier switching", async () => { await session.disconnect(); }, 120_000); - it("should preserve effective tier when fast activation fails", async () => { + it("should preserve effective tier when fast activation fails", async ({ skip }) => { + skip(isByokBackend); const session = await client.createSession({ onPermissionRequest: approveAll, model: "auto", diff --git a/nodejs/test/e2e/builtin_tools.e2e.test.ts b/nodejs/test/e2e/builtin_tools.e2e.test.ts index 39900bc7d6..174af3e9ac 100644 --- a/nodejs/test/e2e/builtin_tools.e2e.test.ts +++ b/nodejs/test/e2e/builtin_tools.e2e.test.ts @@ -3,9 +3,10 @@ *--------------------------------------------------------------------------------------------*/ import { writeFile, mkdir } from "fs/promises"; -import { join } from "path"; +import { join, normalize } from "path"; import { describe, expect, it } from "vitest"; import { approveAll } from "../../src/index.js"; +import type { ToolExecutionCompleteEvent } from "../../src/generated/session-events.js"; import { createSdkTestContext } from "./harness/sdkTestContext"; // Built-in tool tests spawn a real CLI subprocess and execute actual shell / @@ -93,8 +94,18 @@ describe("Built-in Tools", async () => { it( "should edit a file successfully", async () => { - await writeFile(join(workDir, "edit_me.txt"), "Hello World\nGoodbye World\n"); + const path = join(workDir, "edit_me.txt"); + await writeFile(path, "Hello World\nGoodbye World\n"); const session = await client.createSession({ onPermissionRequest: approveAll }); + const completions: ToolExecutionCompleteEvent["data"][] = []; + let editToolCallId: string | undefined; + session.on((event) => { + if (event.type === "tool.execution_start" && event.data.toolName === "edit") { + editToolCallId = event.data.toolCallId; + } else if (event.type === "tool.execution_complete") { + completions.push(event.data); + } + }); const msg = await session.sendAndWait( { prompt: "Edit the file 'edit_me.txt': replace 'Hello World' with 'Hi Universe'. Then read it back and tell me its contents.", @@ -102,6 +113,16 @@ describe("Built-in Tools", async () => { SEND_TIMEOUT_MS ); expect(msg?.data.content).toContain("Hi Universe"); + expect(editToolCallId).toBeDefined(); + const editEvent = completions.find((event) => event.toolCallId === editToolCallId); + expect(editEvent?.success).toBe(true); + // Local Windows paths may contain both separator spellings. + expect( + editEvent?.fileEdits?.map((effect) => ({ + ...effect, + path: normalize(effect.path), + })) + ).toEqual([{ path, kind: "edit" }]); }, TEST_TIMEOUT_MS ); @@ -111,7 +132,17 @@ describe("Built-in Tools", async () => { it( "should create a new file", async () => { + const path = join(workDir, "new_file.txt"); const session = await client.createSession({ onPermissionRequest: approveAll }); + const completions: ToolExecutionCompleteEvent["data"][] = []; + let createToolCallId: string | undefined; + session.on((event) => { + if (event.type === "tool.execution_start" && event.data.toolName === "create") { + createToolCallId = event.data.toolCallId; + } else if (event.type === "tool.execution_complete") { + completions.push(event.data); + } + }); const msg = await session.sendAndWait( { prompt: "Create a file called 'new_file.txt' with the content 'Created by test'. Then read it back to confirm.", @@ -119,6 +150,50 @@ describe("Built-in Tools", async () => { SEND_TIMEOUT_MS ); expect(msg?.data.content).toContain("Created by test"); + expect(createToolCallId).toBeDefined(); + const createEvent = completions.find( + (event) => event.toolCallId === createToolCallId + ); + expect(createEvent?.success).toBe(true); + const createFileEdits = createEvent?.fileEdits; + expect( + createFileEdits?.map((effect) => ({ ...effect, path: normalize(effect.path) })) + ).toEqual([{ path, kind: "create" }]); + + const sessionId = session.sessionId; + await client.rpc.sessions.save({ sessionId }); + const eventLog = await session.rpc.eventLog.read({ + max: 100, + waitMs: 0, + types: ["tool.execution_complete"], + }); + const persisted = eventLog.events.find( + (event) => + event.type === "tool.execution_complete" && + event.data.toolCallId === createEvent?.toolCallId + ); + expect(persisted).toMatchObject({ + type: "tool.execution_complete", + data: { + toolCallId: createEvent?.toolCallId, + fileEdits: createFileEdits, + }, + }); + await session.disconnect(); + const resumed = await client.resumeSession(sessionId, { + onPermissionRequest: approveAll, + }); + const replayed = (await resumed.getEvents()).find( + (event) => event.id === persisted?.id + ); + expect(replayed).toMatchObject({ + type: "tool.execution_complete", + data: { + toolCallId: createEvent?.toolCallId, + fileEdits: createFileEdits, + }, + }); + await resumed.disconnect(); }, TEST_TIMEOUT_MS ); diff --git a/nodejs/test/e2e/client_options.e2e.test.ts b/nodejs/test/e2e/client_options.e2e.test.ts index b32965ec0f..544ab712e7 100644 --- a/nodejs/test/e2e/client_options.e2e.test.ts +++ b/nodejs/test/e2e/client_options.e2e.test.ts @@ -206,7 +206,7 @@ function getArray(value: unknown): unknown[] { } describe("Client options", async () => { - const { copilotClient: defaultClient, env, workDir } = await createSdkTestContext(); + const { createClient, env, workDir } = await createSdkTestContext(); it("createSession starts the client lazily", async () => { const client = new CopilotClient({ @@ -260,11 +260,7 @@ describe("Client options", async () => { fs.mkdirSync(clientCwd, { recursive: true }); fs.writeFileSync(path.join(clientCwd, "marker.txt"), "I am in the client cwd"); - // Reference defaultClient to keep the shared test context (and its CAPI proxy/env) - // alive for the duration of this test; we deliberately spin up a fresh client with - // a custom cwd to assert that the custom cwd is honored. - void defaultClient; - const client = new CopilotClient({ + const client = createClient({ workingDirectory: clientCwd, env, connection: RuntimeConnection.forStdio({ path: process.env.COPILOT_CLI_PATH }), diff --git a/nodejs/test/e2e/compaction.e2e.test.ts b/nodejs/test/e2e/compaction.e2e.test.ts index a74878101d..04de4d1d4a 100644 --- a/nodejs/test/e2e/compaction.e2e.test.ts +++ b/nodejs/test/e2e/compaction.e2e.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from "vitest"; import { approveAll, type CopilotSession, type SessionEvent } from "../../src/index.js"; -import { createSdkTestContext } from "./harness/sdkTestContext.js"; +import { createSdkTestContext, isCI } from "./harness/sdkTestContext.js"; const compactionTimeoutMs = 60_000; @@ -35,7 +35,7 @@ function getNextSessionEvent( } describe("Compaction", async () => { - const { copilotClient: client } = await createSdkTestContext(); + const { copilotClient: client, openAiEndpoint } = await createSdkTestContext(); it("should trigger compaction with low threshold and emit events", async () => { // Create session with very low compaction thresholds to trigger compaction quickly @@ -84,7 +84,23 @@ describe("Compaction", async () => { expect(startEvent.data.conversationTokens ?? 0).toBeGreaterThan(0); expect(completeEvent.data.success).toBe(true); expect(completeEvent.data.compactionTokensUsed).toBeDefined(); - expect(completeEvent.data.compactionTokensUsed?.inputTokens ?? 0).toBeGreaterThan(0); + // Replay has no prompt usage; recording must preserve the provider's value or absence. + if (isCI) { + expect(completeEvent.data.compactionTokensUsed?.inputTokens ?? 0).toBe(0); + } else { + const exchanges = await openAiEndpoint.getExchanges(); + const compactions = exchanges.filter( + ({ compactionUsage }) => + compactionUsage && compactionUsage.summary === completeEvent.data.summaryContent + ); + expect( + compactions, + "Expected one provider compaction chain for the completed summary" + ).toHaveLength(1); + expect(completeEvent.data.compactionTokensUsed?.inputTokens).toBe( + compactions[0].compactionUsage?.inputTokens + ); + } const summary = (completeEvent.data.summaryContent ?? "").toLowerCase(); expect(summary).toContain(""); expect(summary).toContain(""); diff --git a/nodejs/test/e2e/copilot_request_handler.e2e.test.ts b/nodejs/test/e2e/copilot_request_handler.e2e.test.ts index 04bccb7e08..af79d84ae3 100644 --- a/nodejs/test/e2e/copilot_request_handler.e2e.test.ts +++ b/nodejs/test/e2e/copilot_request_handler.e2e.test.ts @@ -10,9 +10,11 @@ import { approveAll, CopilotRequestHandler, CopilotWebSocketForwarder, + defineTool, type CopilotRequestContext, } from "../../src/index.js"; import { createSdkTestContext } from "./harness/sdkTestContext.js"; +import { waitForCondition } from "./harness/sdkTestHelper.js"; const HTTP_TEXT = "OK from synthetic HTTP upstream."; const WS_TEXT = "OK from synthetic WS upstream."; @@ -32,9 +34,13 @@ async function startFakeUpstream(): Promise<{ url: string; server: HttpServer; wsRequestCount: () => number; + wsBodies: () => Array>; + httpBodies: () => Array>; close: () => Promise; }> { let wsRequests = 0; + const wsBodies: Array> = []; + const httpBodies: Array> = []; const httpServer = createServer((req, res) => { const url = new URL(req.url ?? "/", `http://${req.headers.host ?? "localhost"}`); @@ -66,6 +72,30 @@ async function startFakeUpstream(): Promise<{ }, }, }, + { + id: "gpt-6-sol", + name: "GPT-6 Sol", + object: "model", + vendor: "OpenAI", + version: "1", + preview: false, + model_picker_enabled: true, + supported_endpoints: ["/responses", "ws:/responses"], + capabilities: { + type: "chat", + family: "gpt-6-sol", + tokenizer: "o200k_base", + limits: { + max_context_window_tokens: 200000, + max_output_tokens: 8192, + }, + supports: { + streaming: true, + tool_calls: true, + parallel_tool_calls: true, + }, + }, + }, ], }); return; @@ -79,15 +109,20 @@ async function startFakeUpstream(): Promise<{ return; } if (url.pathname.endsWith("/responses") && req.method === "POST") { - // Single-shot HTTP inference (e.g. title generation). SSE - // events the `responses-client.ts` reducer accepts. drainBody(req) - .then(() => { + .then((raw) => { + const body = JSON.parse(raw.toString()) as Record; + httpBodies.push(body); + const events = buildResponsesEvents(HTTP_TEXT, "resp_stub_http"); + if (body.stream !== true) { + sendJson(res, 200, events.at(-1)!.response); + return; + } res.writeHead(200, { "content-type": "text/event-stream", "cache-control": "no-cache", }); - for (const event of buildResponsesEvents(HTTP_TEXT, "resp_stub_http")) { + for (const event of events) { res.write(`event: ${event.type}\ndata: ${JSON.stringify(event)}\n\n`); } res.end(); @@ -106,13 +141,73 @@ async function startFakeUpstream(): Promise<{ wss.on("connection", (socket) => { socket.on("message", (raw) => { wsRequests++; - // For each `response.create` request the runtime sends, - // answer with the ordered `/responses` event objects — one - // event per outbound WS message, raw JSON (NOT SSE-framed). - for (const event of buildResponsesEvents(WS_TEXT, "resp_stub_ws")) { + const body = JSON.parse(raw.toString()) as Record; + wsBodies.push(body); + const input = JSON.stringify(body.input); + const id = `resp_stub_ws_${wsRequests}`; + const positionedUpdate = + Array.isArray(body.input) && + body.input.some((item: { type?: string }) => item.type === "additional_tools"); + const rejection = { + type: "error", + error: { + code: "bad_request", + type: "websocket_error", + message: "additional_tools rejected", + }, + }; + const restricted = + (body.tool_choice as { type?: string } | undefined)?.type === "allowed_tools"; + let events: Array>; + if (restricted && input.includes("SDK_ALLOWLIST_REJECT_BEFORE")) { + events = [ + { + ...rejection, + error: { + ...rejection.error, + code: "invalid_request_body", + message: "allowed_tools rejected", + }, + }, + ]; + } else if (restricted && input.includes("SDK_ALLOWLIST_REJECT_AFTER")) { + events = [ + ...buildResponsesEvents("already streamed", id).slice(0, 4), + { + ...rejection, + error: { + ...rejection.error, + code: "invalid_request_body", + message: "allowed_tools rejected", + }, + }, + ]; + } else if (input.includes("SDK_REMOVAL_RESTORED")) { + events = buildResponsesEvents("SDK_REMOVAL_RESTORED_OK", id); + } else if (input.includes("SDK_ALLOWLIST_REJECT_BEFORE")) { + events = buildResponsesEvents("SDK_ALLOWLIST_RECOVERED", id); + } else if (positionedUpdate && input.includes("SDK_RESPONSES_REJECT_BEFORE")) { + events = [rejection]; + } else if (positionedUpdate && input.includes("SDK_RESPONSES_REJECT_AFTER")) { + events = [...buildResponsesEvents("already streamed", id).slice(0, 4), rejection]; + } else if (input.includes("SDK_RESPONSES_LEGACY_FOLLOW_UP")) { + events = buildResponsesEvents("SDK_RESPONSES_LEGACY_OK", id); + } else if (input.includes("SDK_RESPONSES_REJECT_BEFORE")) { + events = buildResponsesEvents("SDK_RESPONSES_RECOVERED", id); + } else if ( + input.includes("SDK_RESPONSES_AFTER") && + !input.includes("SDK_ADDED_TOOL_RESULT") + ) { + events = buildFunctionCallEvents(id); + } else { + events = buildResponsesEvents( + input.includes("SDK_ADDED_TOOL_RESULT") ? "SDK_RESPONSES_TOOL_OK" : WS_TEXT, + id + ); + } + for (const event of events) { socket.send(JSON.stringify(event)); } - void raw; }); }); @@ -124,6 +219,8 @@ async function startFakeUpstream(): Promise<{ url, server: httpServer, wsRequestCount: () => wsRequests, + wsBodies: () => [...wsBodies], + httpBodies: () => [...httpBodies], async close() { wss.clients.forEach((c) => c.terminate()); await new Promise((resolve) => wss.close(() => resolve())); @@ -132,6 +229,35 @@ async function startFakeUpstream(): Promise<{ }; } +function buildFunctionCallEvents(id: string): Array> { + const call = { + type: "function_call", + id: "fc_sdk_added", + call_id: "call_sdk_added", + name: "sdk_epoch_added", + arguments: "{}", + status: "completed", + }; + return [ + { + type: "response.created", + response: { id, object: "response", status: "in_progress", output: [] }, + }, + { type: "response.output_item.added", output_index: 0, item: call }, + { type: "response.output_item.done", output_index: 0, item: call }, + { + type: "response.completed", + response: { + id, + object: "response", + status: "completed", + output: [call], + usage: { input_tokens: 5, output_tokens: 7, total_tokens: 12 }, + }, + }, + ]; +} + function sendJson(res: ServerResponse, status: number, body: unknown): void { res.writeHead(status, { "content-type": "application/json" }); res.end(JSON.stringify(body)); @@ -347,4 +473,359 @@ describe("CopilotRequestHandler — single subclass handles HTTP + WebSocket", a // Validate the final assistant response arrived (guards against truncated captures) expect(resultJson).toMatch(/OK from synthetic (HTTP|WS) upstream/); }, 90_000); + + it("applies SDK tool additions at the WebSocket boundary without a stale continuation", async () => { + const original = { + name: "sdk_epoch_original", + description: "Original SDK tool", + parameters: { type: "object", properties: {} }, + defer: "never", + } as const; + const added = { + name: "sdk_epoch_added", + description: "Added SDK tool", + parameters: { type: "object", properties: {} }, + defer: "never", + } as const; + let addedToolCalls = 0; + await client.start(); + const beforeRequests = upstream.wsBodies().length; + const session = await client.createSession({ + model: "gpt-6-sol", + onPermissionRequest: approveAll, + capi: { enableWebSocketResponses: true }, + toolSearch: { enabled: false }, + tools: [ + defineTool(original.name, { + description: original.description, + handler: () => "ORIGINAL_TOOL_RESULT", + }), + defineTool(added.name, { + description: added.description, + handler: () => { + addedToolCalls++; + return "SDK_ADDED_TOOL_RESULT"; + }, + }), + ], + }); + try { + await session.rpc.tools.initializeAndValidate(); + await session.rpc.tools.set({ tools: [original] }); + const first = await session.sendAndWait({ prompt: "SDK_RESPONSES_FIRST" }); + expect(first?.data.content).toContain(WS_TEXT); + + await session.rpc.tools.set({ tools: [original, added] }); + const second = await session.sendAndWait({ + prompt: "SDK_RESPONSES_AFTER: use sdk_epoch_added now.", + }); + // Validate the final assistant response arrived (guards against truncated captures) + expect(second?.data.content).toContain("SDK_RESPONSES_TOOL_OK"); + expect(addedToolCalls).toBe(1); + + const requests = upstream.wsBodies().slice(beforeRequests); + expect(requests).toHaveLength(3); + const [before, update, afterTool] = requests; + expect(update.tools).toEqual(before.tools); + expect(update.previous_response_id ?? null).toBeNull(); + const input = update.input as Array>; + expect(input).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + type: "additional_tools", + tools: expect.arrayContaining([ + expect.objectContaining({ name: added.name }), + ]), + }), + ]) + ); + const additionIndex = input.findIndex((item) => item.type === "additional_tools"); + expect(additionIndex).toBeGreaterThan(0); + expect(JSON.stringify(input.slice(0, additionIndex))).toContain("SDK_RESPONSES_AFTER"); + expect(JSON.stringify(input.slice(additionIndex + 1))).not.toContain( + "SDK_RESPONSES_AFTER" + ); + expect(JSON.stringify(afterTool.input)).toContain("SDK_ADDED_TOOL_RESULT"); + } finally { + await session.disconnect(); + } + }, 90_000); + + it("keeps the SDK removal prefix while restricting tools across continuations and restoration", async () => { + const original = { + name: "sdk_epoch_original", + description: "Original SDK tool", + parameters: { type: "object", properties: {} }, + defer: "never", + } as const; + const added = { + ...original, + name: "sdk_epoch_added", + description: "Added SDK tool", + } as const; + let survivorCalls = 0; + await client.start(); + const beforeRequests = upstream.wsBodies().length; + const session = await client.createSession({ + model: "gpt-6-sol", + onPermissionRequest: approveAll, + capi: { enableWebSocketResponses: true }, + toolSearch: { enabled: false }, + availableTools: [original.name, added.name], + tools: [ + defineTool(original.name, { + description: original.description, + handler: () => { + throw new Error("Removed SDK tool was executed"); + }, + }), + defineTool(added.name, { + description: added.description, + handler: () => { + survivorCalls++; + return "SDK_ADDED_TOOL_RESULT"; + }, + }), + ], + }); + try { + await session.rpc.tools.initializeAndValidate(); + await session.rpc.tools.set({ tools: [original, added] }); + expect( + (await session.sendAndWait({ prompt: "SDK_REMOVAL_FIRST" }))?.data.content + ).toContain(WS_TEXT); + await session.rpc.tools.set({ tools: [added] }); + const used = await session.sendAndWait({ + prompt: "SDK_RESPONSES_AFTER: use sdk_epoch_added now.", + }); + expect(used?.data.content).toContain("SDK_RESPONSES_TOOL_OK"); + expect(survivorCalls).toBe(1); + let requests = upstream.wsBodies().slice(beforeRequests); + expect(requests).toHaveLength(3); + const [before, removed, afterTool] = requests; + const choice = { + type: "allowed_tools", + mode: "auto", + tools: [{ type: "function", name: added.name }], + }; + expect(removed.tools).toEqual(before.tools); + expect(removed.instructions).toEqual(before.instructions); + expect(removed.tool_choice).toEqual(choice); + expect(afterTool.tool_choice).toEqual(choice); + expect(removed.previous_response_id ?? null).toBeNull(); + expect(JSON.stringify(removed.input)).toContain("SDK_REMOVAL_FIRST"); + + await session.rpc.tools.set({ tools: [] }); + expect( + (await session.sendAndWait({ prompt: "SDK_REMOVAL_ALL" }))?.data.content + ).toContain(HTTP_TEXT); + // No remaining tools selects the runtime's single-shot HTTP route. + const allRemoved = upstream + .httpBodies() + .findLast((body) => JSON.stringify(body.input).includes("SDK_REMOVAL_ALL"))!; + expect(allRemoved.tools).toEqual([]); + expect(allRemoved.tool_choice).toBeUndefined(); + expect(allRemoved.previous_response_id ?? null).toBeNull(); + + await session.rpc.tools.set({ tools: [original, added] }); + const restored = await session.sendAndWait({ prompt: "SDK_REMOVAL_RESTORED" }); + // Validate the final assistant response arrived (guards against truncated captures) + expect(restored?.data.content).toContain("SDK_REMOVAL_RESTORED_OK"); + requests = upstream.wsBodies().slice(beforeRequests); + const last = requests.at(-1)!; + expect(last.tools).toEqual(before.tools); + expect(last.tool_choice).not.toEqual( + expect.objectContaining({ type: "allowed_tools" }) + ); + expect(last.previous_response_id ?? null).toBeNull(); + expect(JSON.stringify(last.input)).not.toContain('"type":"additional_tools"'); + } finally { + await session.disconnect(); + } + }, 90_000); + + for (const phase of ["BEFORE", "AFTER"] as const) { + it(`only replays a rejected SDK tool allowlist ${phase.toLowerCase()} output`, async () => { + const original = { + name: "sdk_epoch_original", + description: "Original SDK tool", + parameters: { type: "object", properties: {} }, + defer: "never", + } as const; + const added = { + ...original, + name: "sdk_epoch_added", + description: "Added SDK tool", + } as const; + await client.start(); + const beforeRequests = upstream.wsBodies().length; + const session = await client.createSession({ + model: "gpt-6-sol", + onPermissionRequest: approveAll, + capi: { enableWebSocketResponses: true }, + toolSearch: { enabled: false }, + availableTools: [original.name, added.name], + tools: [original, added].map((tool) => + defineTool(tool.name, { + description: tool.description, + handler: () => "UNUSED", + }) + ), + }); + let failedTurnIdle = false; + const unsubscribe = session.on((event) => { + if (event.type === "session.idle" && !event.agentId) { + failedTurnIdle = true; + } + }); + try { + await session.rpc.tools.initializeAndValidate(); + await session.rpc.tools.set({ tools: [original, added] }); + expect( + (await session.sendAndWait({ prompt: "SDK_ALLOWLIST_FIRST" }))?.data.content + ).toContain(WS_TEXT); + failedTurnIdle = false; + await session.rpc.tools.set({ tools: [added] }); + const changed = session.sendAndWait({ prompt: `SDK_ALLOWLIST_REJECT_${phase}` }); + if (phase === "BEFORE") { + expect((await changed)?.data.content).toContain("SDK_ALLOWLIST_RECOVERED"); + } else { + await expect(changed).rejects.toThrow(/allowed_tools rejected/); + await waitForCondition(() => failedTurnIdle, { + timeoutMessage: "Rejected SDK allowlist did not reach session.idle", + }); + } + const requests = upstream.wsBodies().slice(beforeRequests); + expect(requests).toHaveLength(phase === "BEFORE" ? 3 : 2); + expect(requests[1].tools).toEqual(requests[0].tools); + expect(requests[1].tool_choice).toMatchObject({ type: "allowed_tools" }); + expect(requests[1].previous_response_id ?? null).toBeNull(); + if (phase === "BEFORE") { + expect(requests[2].tool_choice).not.toEqual( + expect.objectContaining({ type: "allowed_tools" }) + ); + expect(JSON.stringify(requests[2].tools)).not.toContain(original.name); + expect(requests[2].previous_response_id ?? null).toBeNull(); + } + const followUp = await session.sendAndWait({ + prompt: "SDK_RESPONSES_LEGACY_FOLLOW_UP", + }); + // Validate the final assistant response arrived (guards against truncated captures) + expect(followUp?.data.content).toContain("SDK_RESPONSES_LEGACY_OK"); + const last = upstream.wsBodies().at(-1)!; + expect(last.tool_choice).not.toEqual( + expect.objectContaining({ type: "allowed_tools" }) + ); + expect(JSON.stringify(last.tools)).not.toContain(original.name); + } finally { + unsubscribe(); + await session.disconnect(); + } + }, 90_000); + } + + for (const phase of ["BEFORE", "AFTER"] as const) { + it(`only replays a rejected SDK WebSocket tool update ${phase.toLowerCase()} output`, async () => { + const original = { + name: "sdk_epoch_original", + description: "Original SDK tool", + parameters: { type: "object", properties: {} }, + defer: "never", + } as const; + const added = { + name: "sdk_epoch_added", + description: "Added SDK tool", + parameters: { type: "object", properties: {} }, + defer: "never", + } as const; + await client.start(); + const beforeRequests = upstream.wsBodies().length; + const session = await client.createSession({ + model: "gpt-6-sol", + onPermissionRequest: approveAll, + capi: { enableWebSocketResponses: true }, + toolSearch: { enabled: false }, + tools: [ + defineTool(original.name, { + description: original.description, + handler: () => "ORIGINAL_TOOL_RESULT", + }), + defineTool(added.name, { + description: added.description, + handler: () => "SDK_ADDED_TOOL_RESULT", + }), + ], + }); + let failedTurnIdle = false; + const unsubscribe = session.on((event) => { + if (event.type === "session.idle" && !event.agentId) { + failedTurnIdle = true; + } + }); + try { + await session.rpc.tools.initializeAndValidate(); + await session.rpc.tools.set({ tools: [original] }); + const first = await session.sendAndWait({ prompt: "SDK_RESPONSES_FIRST" }); + expect(first?.data.content).toContain(WS_TEXT); + failedTurnIdle = false; + + await session.rpc.tools.set({ tools: [original, added] }); + const changedTurn = session.sendAndWait({ + prompt: `SDK_RESPONSES_REJECT_${phase}: use sdk_epoch_added now.`, + }); + if (phase === "BEFORE") { + const recovered = await changedTurn; + expect(recovered?.data.content).toContain("SDK_RESPONSES_RECOVERED"); + } else { + await expect(changedTurn).rejects.toThrow(/additional_tools rejected/); + await waitForCondition(() => failedTurnIdle, { + timeoutMessage: "Rejected SDK tool update did not reach session.idle", + }); + } + + const requests = upstream.wsBodies().slice(beforeRequests); + expect(requests).toHaveLength(phase === "BEFORE" ? 3 : 2); + const [before, rejected, retry] = requests; + expect(rejected.tools).toEqual(before.tools); + expect(rejected.previous_response_id ?? null).toBeNull(); + const positioned = (rejected.input as Array<{ type?: string }>).filter( + (item) => item.type === "additional_tools" + ); + expect(positioned).toHaveLength(1); + expect(JSON.stringify(positioned[0])).toContain(added.name); + if (phase === "BEFORE") { + expect(retry?.previous_response_id ?? null).toBeNull(); + expect(JSON.stringify(retry?.input)).not.toContain('"type":"additional_tools"'); + expect(JSON.stringify(retry?.tools)).toContain(added.name); + + const followUp = await session.sendAndWait({ + prompt: "SDK_RESPONSES_LEGACY_FOLLOW_UP", + }); + expect(followUp?.data.content).toContain("SDK_RESPONSES_LEGACY_OK"); + const followUpRequest = upstream.wsBodies().at(-1); + expect(JSON.stringify(followUpRequest?.tools)).toEqual( + JSON.stringify(retry?.tools) + ); + expect(JSON.stringify(followUpRequest?.input)).not.toContain( + '"type":"additional_tools"' + ); + } else { + const followUp = await session.sendAndWait({ + prompt: "SDK_RESPONSES_LEGACY_FOLLOW_UP", + }); + expect(followUp?.data.content).toContain("SDK_RESPONSES_LEGACY_OK"); + const followUpRequest = upstream.wsBodies().at(-1); + expect(upstream.wsBodies().slice(beforeRequests)).toHaveLength(3); + expect(followUpRequest?.previous_response_id ?? null).toBeNull(); + expect(JSON.stringify(followUpRequest?.tools)).toContain(added.name); + expect(JSON.stringify(followUpRequest?.input)).not.toContain( + '"type":"additional_tools"' + ); + } + } finally { + unsubscribe(); + await session.disconnect(); + } + }, 90_000); + } }); diff --git a/nodejs/test/e2e/customizations_reload.e2e.test.ts b/nodejs/test/e2e/customizations_reload.e2e.test.ts new file mode 100644 index 0000000000..2f33e04791 --- /dev/null +++ b/nodejs/test/e2e/customizations_reload.e2e.test.ts @@ -0,0 +1,58 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +import { execFileSync } from "node:child_process"; +import { mkdirSync, writeFileSync } from "node:fs"; +import { join } from "node:path"; +import { describe, expect, it } from "vitest"; +import { approveAll } from "../../src/index.js"; +import { createSdkTestContext } from "./harness/sdkTestContext.js"; + +describe("Session customization reload", async () => { + const { copilotClient: client, workDir } = await createSdkTestContext(); + + it("discovers repository instructions created after session startup without changing its cwd", async () => { + execFileSync("git", ["init", "--quiet"], { cwd: workDir }); + const session = await client.createSession({ onPermissionRequest: approveAll }); + + try { + const before = await session.rpc.metadata.snapshot(); + const marker = "SDK_RELOAD_NEW_REPOSITORY_INSTRUCTIONS"; + expect( + (await session.rpc.instructions.getSources()).sources.some((source) => + source.content.includes(marker) + ) + ).toBe(false); + + const instructionsDir = join(workDir, ".github"); + mkdirSync(instructionsDir); + writeFileSync( + join(instructionsDir, "copilot-instructions.md"), + `Repository instructions: ${marker}\n` + ); + + const reload = await session.rpc.customizations.reload(); + expect(reload.errors).toEqual([]); + expect(reload.outcomes).toEqual( + expect.arrayContaining([ + expect.objectContaining({ subsystem: "repositoryContext", status: "reloaded" }), + expect.objectContaining({ subsystem: "instructions", status: "reloaded" }), + ]) + ); + expect( + (await session.rpc.instructions.getSources()).sources.some( + (source) => + source.type === "repo" && + source.location === "repository" && + source.content.includes(marker) + ) + ).toBe(true); + expect((await session.rpc.metadata.snapshot()).workingDirectory).toBe( + before.workingDirectory + ); + } finally { + await session.disconnect(); + } + }); +}); diff --git a/nodejs/test/e2e/disabled_mcp_servers.e2e.test.ts b/nodejs/test/e2e/disabled_mcp_servers.e2e.test.ts index 8478f93288..d9e8cee28c 100644 --- a/nodejs/test/e2e/disabled_mcp_servers.e2e.test.ts +++ b/nodejs/test/e2e/disabled_mcp_servers.e2e.test.ts @@ -395,6 +395,8 @@ describe("disabled MCP servers", async () => { const disabledSession = await client.createSession({ onPermissionRequest: approveAll, enableConfigDiscovery: true, + // This exercises MCP discovery, not standalone extension hosting. + requestExtensions: false, enableMcpApps: true, githubMcpToolConfig: { enableAllTools: true }, disabledMcpServers: ["github-mcp-server"], @@ -420,6 +422,7 @@ describe("disabled MCP servers", async () => { await using enabledSession = await client.createSession({ onPermissionRequest: approveAll, enableConfigDiscovery: true, + requestExtensions: false, enableMcpApps: true, githubMcpToolConfig: { enableAllTools: true }, }); diff --git a/nodejs/test/e2e/harness/CapiProxy.test.ts b/nodejs/test/e2e/harness/CapiProxy.test.ts index 71a77cc1df..1c804d7e50 100644 --- a/nodejs/test/e2e/harness/CapiProxy.test.ts +++ b/nodejs/test/e2e/harness/CapiProxy.test.ts @@ -285,11 +285,11 @@ server.listen(0, "127.0.0.1", () => { const proxy = new CapiProxy(); const starting = proxy.start(); const child = vi.mocked(spawn).mock.results[0].value; - const outputLines = createInterface({ input: child.stdout! }); + let outputLines: ReturnType | undefined; let stopping: Promise | undefined; onTestFinished(async () => { vi.useRealTimers(); - outputLines.close(); + outputLines?.close(); try { await stopChildProcess(child); await Promise.allSettled([starting, ...(stopping ? [stopping] : [])]); @@ -298,8 +298,8 @@ server.listen(0, "127.0.0.1", () => { } }); const proxyUrl = await starting; - // The proxy's startup reader closes and pauses the shared stdout stream. - child.stdout!.resume(); + // Startup closes its reader and pauses stdout; attach the flush reader afterward. + outputLines = createInterface({ input: child.stdout! }); const flushBlocked = once(outputLines, "line"); let resolveExitWait!: () => void; const exitWaitStarted = new Promise((resolve) => { diff --git a/nodejs/test/e2e/harness/CapiProxy.ts b/nodejs/test/e2e/harness/CapiProxy.ts index 3f5bb63cc2..a59591b75f 100644 --- a/nodejs/test/e2e/harness/CapiProxy.ts +++ b/nodejs/test/e2e/harness/CapiProxy.ts @@ -5,7 +5,7 @@ import { spawn, type ChildProcess } from "child_process"; import { createInterface } from "readline"; import { expect, inject } from "vitest"; -import type { CapturedRequest } from "../../../../test/harness/replayingCapiProxy"; +import type { CapturedRequest, ReplayBackend } from "../../../../test/harness/replayingCapiProxy"; import type { CopilotUserResponse, MemoryApiStub, @@ -14,6 +14,7 @@ import type { import { isCI } from "./sdkTestContext"; import { CAPI_PROXY_BUNDLE } from "./proxyBundleContext"; import { hasChildExited, stopChildProcess, waitForChildExit } from "./sdkTestHelper"; +import { testBackend } from "./testBackend"; const NO_PROXY = "127.0.0.1,localhost,::1"; @@ -63,6 +64,8 @@ export class CapiProxy { serverProcess.off("exit", onExit); serverProcess.off("error", onError); lineReader.close(); + // Closing readline pauses stdout even when another reader still owns it. + stdout.resume(); }; const onLine = (line: string) => { lines.push(line); @@ -145,12 +148,14 @@ export class CapiProxy { async updateConfig(config: { filePath: string; workDir: string; + backend?: ReplayBackend; testInfo?: { file: string; line?: number }; + modelNames?: Record; }): Promise { const response = await fetch(`${this.proxyUrl}/config`, { method: "POST", headers: { "content-type": "application/json" }, - body: JSON.stringify(config), + body: JSON.stringify({ backend: testBackend, ...config }), }); expect(response.ok).toBe(true); } diff --git a/nodejs/test/e2e/harness/sdkTestContext.test.ts b/nodejs/test/e2e/harness/sdkTestContext.test.ts new file mode 100644 index 0000000000..3503e1080f --- /dev/null +++ b/nodejs/test/e2e/harness/sdkTestContext.test.ts @@ -0,0 +1,52 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +import { existsSync } from "node:fs"; +import { mkdir, readFile, writeFile } from "node:fs/promises"; +import { join } from "node:path"; +import { rimraf } from "rimraf"; +import { afterAll, describe, expect, it, onTestFinished, vi } from "vitest"; +import { createSdkTestContext } from "./sdkTestContext"; + +vi.mock("./CapiProxy", () => ({ + CapiProxy: class { + start = vi.fn().mockResolvedValue("http://127.0.0.1:1"); + getProxyEnv = vi.fn().mockReturnValue({}); + setCopilotUserByToken = vi.fn().mockResolvedValue(undefined); + updateConfig = vi.fn().mockResolvedValue(undefined); + stop = vi.fn().mockResolvedValue(undefined); + }, +})); + +vi.mock("rimraf", async (importOriginal) => { + const actual = await importOriginal(); + return { ...actual, rimraf: vi.fn(actual.rimraf) }; +}); + +describe("SDK test context cleanup", async () => { + let workDir: string; + afterAll(() => { + expect(rimraf).toHaveBeenCalled(); + expect(existsSync(workDir)).toBe(false); + }); + const context = await createSdkTestContext(); + workDir = context.workDir; + + it("keeps working files available until test-owned client cleanup finishes", async () => { + const client = context.createClient(); + const logsDir = join(workDir, "logs"); + const logPath = join(logsDir, "client.log"); + onTestFinished(async () => { + try { + expect(rimraf).not.toHaveBeenCalled(); + expect(await readFile(logPath, "utf8")).toBe("client is running"); + await writeFile(logPath, "client is stopped"); + } finally { + await client.stop(); + } + }); + await mkdir(logsDir); + await writeFile(logPath, "client is running"); + }); +}); diff --git a/nodejs/test/e2e/harness/sdkTestContext.ts b/nodejs/test/e2e/harness/sdkTestContext.ts index 479443e651..4a54acca93 100644 --- a/nodejs/test/e2e/harness/sdkTestContext.ts +++ b/nodejs/test/e2e/harness/sdkTestContext.ts @@ -8,11 +8,13 @@ import os from "os"; import { basename, dirname, join, resolve } from "path"; import { rimraf } from "rimraf"; import { fileURLToPath } from "url"; -import { afterAll, afterEach, beforeEach, onTestFailed, TestContext } from "vitest"; +import { afterAll, afterEach, beforeEach, onTestFailed, onTestFinished, TestContext } from "vitest"; import { CopilotClient, CopilotClientOptions, RuntimeConnection } from "../../../src"; +import type { ResumeSessionConfig, SessionConfig } from "../../../src"; import { CapiProxy } from "./CapiProxy"; import { formatError, retry } from "./sdkTestHelper"; import { resolvePreparedRuntimePath } from "../../../scripts/prepare-runtime.js"; +import { testBackend, withTestBackend } from "./testBackend"; export const isCI = process.env.GITHUB_ACTIONS === "true"; export const DEFAULT_GITHUB_TOKEN = "fake-token-for-e2e-tests"; @@ -60,11 +62,13 @@ export async function createSdkTestContext({ logLevel, useStdio, copilotClientOptions, + modelNames, }: { logLevel?: "error" | "none" | "warning" | "info" | "debug" | "all"; cliPath?: string; useStdio?: boolean; copilotClientOptions?: CopilotClientOptions; + modelNames?: Record; } = {}) { const homeDir = realpathSync(fs.mkdtempSync(join(os.tmpdir(), "copilot-test-config-"))); const copilotHomeDir = realpathSync(fs.mkdtempSync(join(os.tmpdir(), "copilot-test-home-"))); @@ -159,6 +163,16 @@ export async function createSdkTestContext({ const mergedEnv = { ...env, ...userEnv }; + class TestClient extends CopilotClient { + override createSession(config: SessionConfig) { + return super.createSession(withTestBackend(config, testBackend, proxyUrl)); + } + + override resumeSession(sessionId: string, config: ResumeSessionConfig) { + return super.resumeSession(sessionId, withTestBackend(config, testBackend, proxyUrl)); + } + } + // The in-process (FFI) transport loads the runtime into this test host process, // and its worker inherits this process's ambient environment rather than a // per-client env block (see https://github.com/github/copilot-sdk/issues/1934). @@ -210,7 +224,7 @@ export async function createSdkTestContext({ } const effectiveInProcess = effectiveConnection.kind === "inprocess"; - return new CopilotClient({ + return new TestClient({ // The in-process transport rejects a per-client workingDirectory (it would have to // mutate the shared host process cwd). Instead the harness changes this process's // cwd to workDir around the in-process worker's startup (see beforeEach below), so @@ -224,7 +238,7 @@ export async function createSdkTestContext({ env: effectiveInProcess ? undefined : mergedEnv, logLevel: logLevel || "error", connection: effectiveConnection, - gitHubToken: authTokenToUse, + gitHubToken: effectiveConnection.kind === "uri" ? undefined : authTokenToUse, ...rest, }); } @@ -248,6 +262,16 @@ export async function createSdkTestContext({ onTestFailed(() => { anyTestFailed = true; }); + // Finished hooks run in reverse order, so test-owned clients stop before files are removed. + onTestFinished(async () => { + // The in-process runtime retains open state files until afterAll shuts it down. + // Keep its isolated home intact while it is alive; removing open files on POSIX + // can leave later tests using unlinked database state. + const cleanupPaths = isInProcess + ? [join(workDir, "*")] + : [join(homeDir, "*"), join(workDir, "*")]; + await rimraf(cleanupPaths, { glob: true }); + }); // Mirror this context's environment onto the real process for in-process // hosting, right before the test runs (see the comment above the client). The @@ -271,6 +295,7 @@ export async function createSdkTestContext({ await openAiEndpoint.updateConfig({ filePath: getTrafficCapturePath(testContext), workDir, + modelNames, testInfo: { file: testContext.task.file.filepath, line: testContext.task.location?.line, @@ -293,13 +318,6 @@ export async function createSdkTestContext({ process.chdir(restoreCwd); restoreCwd = undefined; } - // The in-process runtime retains open state files until afterAll shuts it down. - // Keep its isolated home intact while it is alive; removing open files on POSIX - // can leave later tests using unlinked database state. - const cleanupPaths = isInProcess - ? [join(workDir, "*")] - : [join(homeDir, "*"), join(workDir, "*")]; - await rimraf(cleanupPaths, { glob: true }); }); afterAll(async () => { diff --git a/nodejs/test/e2e/harness/testBackend.ts b/nodejs/test/e2e/harness/testBackend.ts new file mode 100644 index 0000000000..21ab4f8b3e --- /dev/null +++ b/nodejs/test/e2e/harness/testBackend.ts @@ -0,0 +1,49 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +import type { ProviderConfig, SessionConfig } from "../../../src"; +import type { ReplayBackend } from "../../../../test/harness/replayingCapiProxy"; + +export function parseTestBackend(value: string | undefined): ReplayBackend { + const backend = value?.trim().toLowerCase() || "capi"; + switch (backend) { + case "capi": + case "anthropic-messages": + case "openai-responses": + case "openai-completions": + return backend; + default: + throw new Error(`Unsupported COPILOT_SDK_E2E_BACKEND: ${value}`); + } +} + +export const testBackend = parseTestBackend(process.env.COPILOT_SDK_E2E_BACKEND); +export const isByokBackend = testBackend !== "capi"; + +export function withTestBackend>( + config: T, + backend: ReplayBackend, + proxyUrl: string +): T { + if (backend === "capi" || config.provider !== undefined || config.providers !== undefined) { + return config; + } + + const model = + config.model ?? (backend === "anthropic-messages" ? "claude-sonnet-5" : "gpt-4.1"); + const provider: ProviderConfig = { + type: backend === "anthropic-messages" ? "anthropic" : "openai", + wireApi: + backend === "anthropic-messages" + ? undefined + : backend === "openai-responses" + ? "responses" + : "completions", + baseUrl: proxyUrl, + bearerToken: "fake-byok-credential-for-e2e-tests", + modelId: model, + wireModel: model, + }; + return { ...config, model, provider }; +} diff --git a/nodejs/test/e2e/mcp_instruction_recovery.e2e.test.ts b/nodejs/test/e2e/mcp_instruction_recovery.e2e.test.ts new file mode 100644 index 0000000000..9861ad13e9 --- /dev/null +++ b/nodejs/test/e2e/mcp_instruction_recovery.e2e.test.ts @@ -0,0 +1,252 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +import { createServer } from "node:http"; +import { describe, expect, it, onTestFinished } from "vitest"; +import { z } from "zod"; +import { approveAll, defineTool, type CopilotSession, type SessionEvent } from "../../src/index.js"; +import { createSdkTestContext } from "./harness/sdkTestContext.js"; +import { waitForCondition } from "./harness/sdkTestHelper.js"; + +const REPLACEMENT_INSTRUCTIONS = + "CATALOG_RECOVERY_INSTRUCTIONS: Use read_record to retrieve the current record."; +const RECONNECT_RESULT = "Catalog reconnected. Read both server records now."; + +describe("MCP catalog recovery", async () => { + const { copilotClient: client, openAiEndpoint } = await createSdkTestContext(); + + it.each([ + { serverName: "bluebird", surfaced: true, staleCall: false }, + { serverName: "workiq", surfaced: false, staleCall: true }, + ])( + "should recover $serverName instructions without withdrawing peer tools mid-turn", + { timeout: 120_000 }, + async ({ serverName, surfaced, staleCall }) => { + const changing = await startCatalogServer("CATALOG_RECORD"); + const healthy = await startCatalogServer("PEER_RECORD"); + let session: CopilotSession; + let reconnects = 0; + const reconnect = async () => { + reconnects++; + changing.instructions = REPLACEMENT_INSTRUCTIONS; + await session.rpc.mcp.restartServer({ serverName }); + await waitForConnected(session, serverName); + }; + session = await client.createSession({ + onPermissionRequest: approveAll, + availableTools: [ + ...(!staleCall ? ["reconnect_catalog"] : []), + `${serverName}-read_record`, + "healthy-read_record", + ], + mcpServers: { + [serverName]: { type: "http", url: changing.url, tools: ["*"] }, + healthy: { type: "http", url: healthy.url, tools: ["*"] }, + }, + hooks: staleCall + ? { + onPreMcpToolCall: async (input) => { + if ( + input.serverName === serverName && + input.toolName === "read_record" && + reconnects === 0 + ) { + // Invalidate the selected catalog before its invocation dispatches. + await reconnect(); + } + }, + } + : undefined, + tools: staleCall + ? [] + : [ + defineTool("reconnect_catalog", { + description: + "Reconnect the changing catalog before reading either server.", + parameters: z.object({}), + handler: async () => { + await reconnect(); + return RECONNECT_RESULT; + }, + }), + ], + }); + onTestFinished(() => session.disconnect()); + const events: SessionEvent[] = []; + session.on((event) => events.push(event)); + await waitForConnected(session, serverName); + await waitForConnected(session, "healthy"); + + // The instructions change at an awaited tool boundary, not during setup or a new turn. + // Tool definitions stay identical, exercising the instruction-only refresh fast path. + const response = await session.sendAndWait({ + prompt: + (staleCall + ? `Call read_record on ${serverName}. If it reports a changed tool catalog, retry it. ` + : `Call reconnect_catalog exactly once. After it returns, call read_record on ${serverName}. `) + + "then call read_record on healthy. Do not use other tools or invent their outputs. " + + "Finish by reporting both record values and CATALOG_RECOVERY_COMPLETE.", + }); + expect(response?.data.content).toContain("CATALOG_RECOVERY_COMPLETE"); + expect(response?.data.content).toContain("CATALOG_RECORD"); + expect(response?.data.content).toContain("PEER_RECORD"); + expect(reconnects).toBe(1); + expect(changing.calls).toBeGreaterThan(0); + expect(healthy.calls).toBeGreaterThan(0); + expect(changing.initializations).toBe(2); + expect(healthy.initializations).toBe(1); + if (staleCall) { + expect( + events.some( + (event) => + event.type === "tool.execution_complete" && + event.data.success && + event.data.result?.content.includes("CATALOG_RECORD") + ), + "the reconnected server must execute the offered tool" + ).toBe(true); + expect(changing.calls, "only one call may reach the reconnected MCP server").toBe( + 1 + ); + } + + const exchanges = await openAiEndpoint.getExchanges(); + const initial = exchanges[0].request; + expect( + JSON.stringify(initial.messages.filter((message) => message.role === "system")) + ).not.toContain(REPLACEMENT_INSTRUCTIONS); + const afterReconnect = exchanges.filter(({ request }) => + request.messages.some( + (message) => + message.role === "tool" && + JSON.stringify(message.content).includes( + staleCall ? "CATALOG_RECORD" : RECONNECT_RESULT + ) + ) + ); + expect( + afterReconnect.length, + "the same turn must continue after reconnect" + ).toBeGreaterThan(0); + for (const { request } of afterReconnect) { + const toolNames = request.tools + ?.filter((tool) => tool.type === "function") + .map((tool) => tool.function.name); + expect(toolNames).toContain(`${serverName}-read_record`); + expect(toolNames).toContain("healthy-read_record"); + const system = JSON.stringify( + request.messages.filter((message) => message.role === "system") + ); + if (surfaced) { + expect(system).toContain(REPLACEMENT_INSTRUCTIONS); + } else { + expect(system).not.toContain(REPLACEMENT_INSTRUCTIONS); + } + } + } + ); +}); + +async function waitForConnected(session: CopilotSession, serverName: string): Promise { + await waitForCondition( + async () => + (await session.rpc.mcp.list()).servers.some( + (server) => server.name === serverName && server.status === "connected" + ), + { + timeoutMs: 60_000, + timeoutMessage: `${serverName} should connect`, + } + ); +} + +async function startCatalogServer(record: string) { + const state = { instructions: "", calls: 0, initializations: 0, url: "" }; + const server = createServer(async (request, response) => { + if (request.method === "DELETE") { + response.writeHead(200).end(); + return; + } + if (request.method !== "POST") { + response.writeHead(405).end(); + return; + } + const chunks: Buffer[] = []; + for await (const chunk of request) { + chunks.push(Buffer.from(chunk)); + } + const message = JSON.parse(Buffer.concat(chunks).toString("utf8")) as { + id?: string | number; + method: string; + }; + if (message.id === undefined) { + response.writeHead(202).end(); + return; + } + let result: unknown; + switch (message.method) { + case "initialize": + state.initializations++; + result = { + protocolVersion: "2025-03-26", + capabilities: { tools: {} }, + serverInfo: { name: "catalog-recovery", version: "1" }, + ...(state.instructions ? { instructions: state.instructions } : {}), + }; + break; + case "tools/list": + result = { + tools: [ + { + name: "read_record", + description: "Read this server's current record.", + inputSchema: { + type: "object", + properties: {}, + additionalProperties: false, + }, + annotations: { readOnlyHint: true }, + }, + ], + }; + break; + case "tools/call": + state.calls++; + result = { content: [{ type: "text", text: record }], isError: false }; + break; + default: + response.writeHead(200, { "content-type": "application/json" }).end( + JSON.stringify({ + jsonrpc: "2.0", + id: message.id, + error: { code: -32601, message: "Method not found" }, + }) + ); + return; + } + response + .writeHead(200, { "content-type": "application/json" }) + .end(JSON.stringify({ jsonrpc: "2.0", id: message.id, result })); + }); + await new Promise((resolve, reject) => { + server.once("error", reject); + server.listen(0, "127.0.0.1", () => { + server.off("error", reject); + resolve(); + }); + }); + onTestFinished( + () => + new Promise((resolve, reject) => { + server.close((error) => (error ? reject(error) : resolve())); + server.closeAllConnections(); + }) + ); + const address = server.address(); + if (!address || typeof address === "string") { + throw new Error("Expected a TCP MCP server address"); + } + state.url = `http://127.0.0.1:${address.port}/mcp`; + return state; +} diff --git a/nodejs/test/e2e/mcp_legacy_sse_recovery.e2e.test.ts b/nodejs/test/e2e/mcp_legacy_sse_recovery.e2e.test.ts new file mode 100644 index 0000000000..6ba58b3d21 --- /dev/null +++ b/nodejs/test/e2e/mcp_legacy_sse_recovery.e2e.test.ts @@ -0,0 +1,422 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +import { execFileSync } from "node:child_process"; +import { createServer, type ServerResponse } from "node:http"; +import { join } from "node:path"; +import { describe, expect, it, onTestFinished } from "vitest"; +import { z } from "zod"; +import { + approveAll, + CopilotClient, + RuntimeConnection, + type PermissionRequest, + type SessionEvent, +} from "../../src/index.js"; +import { createSdkTestContext } from "./harness/sdkTestContext.js"; +import { waitForCondition } from "./harness/sdkTestHelper.js"; + +const STEP_TIMEOUT_MS = 30_000; +const ACK_TIMEOUT_MS = 2_000; +const rpcRequest = z.object({ + method: z.string(), + id: z.union([z.string(), z.number()]).optional(), + params: z + .object({ + name: z.string().optional(), + arguments: z.object({ marker: z.string() }).optional(), + _meta: z + .object({ progressToken: z.union([z.string(), z.number()]).optional() }) + .optional(), + }) + .optional(), +}); + +/** A completed tool keeps sending progress while its POST acknowledgement stays gated. */ +function createLegacySseServer(replaceDescriptor: boolean) { + const streams = new Map(); + const closedStreams: number[] = []; + const initialized: number[] = []; + const listed: number[] = []; + const calls: Array<{ connection: number; id: string | number; marker: string }> = []; + const errors: unknown[] = []; + let nextConnection = 0; + let heldPost: { connection: number; closed: boolean; headersSent: boolean } | undefined; + let heldProgressTimer: ReturnType | undefined; + let progressSent = 0; + + const server = createServer((request, response) => { + const url = new URL(request.url ?? "", "http://127.0.0.1"); + if (request.method === "GET" && url.pathname === "/sse") { + const connection = ++nextConnection; + streams.set(connection, response); + response.on("close", () => { + closedStreams.push(connection); + streams.delete(connection); + }); + response.writeHead(200, { "Content-Type": "text/event-stream" }); + response.flushHeaders(); + response.write(`event: endpoint\ndata: /messages?connection=${connection}\n\n`); + return; + } + if (request.method !== "POST" || url.pathname !== "/messages") { + response.writeHead(404).end(); + return; + } + const connection = Number(url.searchParams.get("connection")); + request.setEncoding("utf8"); + let body = ""; + request.on("data", (chunk: string) => { + body += chunk; + }); + request.on("error", (error) => errors.push(error)); + request.on("end", () => { + try { + const frame = rpcRequest.parse(JSON.parse(body)); + const stream = streams.get(connection); + if (!stream) { + throw new Error(`POST for closed SSE connection ${connection}`); + } + if (frame.id === undefined) { + response.writeHead(202).end(); + return; + } + let result: unknown; + switch (frame.method) { + case "initialize": + initialized.push(connection); + result = { + protocolVersion: "2024-11-05", + capabilities: { tools: {} }, + serverInfo: { name: "legacy-sse-probe", version: "1.0" }, + }; + break; + case "tools/list": + listed.push(connection); + result = { + tools: [ + { + name: "ping", + description: + replaceDescriptor && connection > 1 + ? "Replacement writable ping" + : "Original read-only ping", + inputSchema: { + type: "object", + // The common projection fills only the root's missing properties object. + ...(connection > 1 ? { properties: {} } : {}), + allOf: [ + { + properties: { marker: { type: "string" } }, + required: ["marker"], + }, + ], + }, + annotations: { + readOnlyHint: !(replaceDescriptor && connection > 1), + }, + }, + ], + }; + break; + case "tools/call": { + if (frame.params?.name !== "ping" || !frame.params.arguments) { + throw new Error(`Unexpected tool call: ${body}`); + } + const marker = frame.params.arguments.marker; + calls.push({ connection, id: frame.id, marker }); + result = { + content: [{ type: "text", text: `SSE_REPLY_${connection}_${marker}` }], + isError: false, + }; + if (marker === "held") { + const token = frame.params._meta?.progressToken; + if (token === undefined) { + throw new Error("The held call did not carry a progress token"); + } + if (heldPost) { + throw new Error("The original gated frame was replayed"); + } + const held = { connection, closed: false, headersSent: false }; + heldPost = held; + response.on("close", () => { + held.headersSent = response.headersSent; + held.closed = true; + clearInterval(heldProgressTimer); + heldProgressTimer = undefined; + }); + stream.write( + `event: message\ndata: ${JSON.stringify({ + jsonrpc: "2.0", + id: frame.id, + result, + })}\n\n` + ); + heldProgressTimer = setInterval(() => { + if (!stream.destroyed && !stream.writableEnded) { + stream.write( + `event: message\ndata: ${JSON.stringify({ + jsonrpc: "2.0", + method: "notifications/progress", + params: { + progressToken: token, + progress: ++progressSent, + }, + })}\n\n` + ); + } + }, 250); + // Only transport cancellation or test teardown can release this POST. + return; + } + break; + } + case "ping": + result = {}; + break; + default: + throw new Error(`Unexpected MCP method: ${frame.method}`); + } + stream.write( + `event: message\ndata: ${JSON.stringify({ + jsonrpc: "2.0", + id: frame.id, + result, + })}\n\n` + ); + response.writeHead(202).end(); + } catch (error) { + errors.push(error); + response.writeHead(500).end(String(error)); + } + }); + }); + + return { + initialized, + listed, + calls, + closedStreams, + get heldPost() { + return heldPost; + }, + get progressSent() { + return progressSent; + }, + assertHealthy() { + if (errors.length) { + throw new AggregateError(errors, "Legacy SSE fixture failed"); + } + }, + async start() { + await new Promise((resolve, reject) => { + server.once("error", reject); + server.listen(0, "127.0.0.1", () => { + server.off("error", reject); + resolve(); + }); + }); + const address = server.address(); + if (!address || typeof address === "string") { + throw new Error("Legacy SSE fixture did not bind a TCP port"); + } + return `http://127.0.0.1:${address.port}/sse`; + }, + async close() { + clearInterval(heldProgressTimer); + server.closeAllConnections(); + if (server.listening) { + await new Promise((resolve, reject) => + server.close((error) => (error ? reject(error) : resolve())) + ); + } + }, + }; +} + +describe("Session-owned legacy SSE acknowledgement recovery", async () => { + const { env: harnessEnv, workDir } = await createSdkTestContext({ + copilotClientOptions: { connection: RuntimeConnection.forStdio() }, + }); + + it.each([ + { replaceDescriptor: false, name: "reconnects without replay and invokes the next tool" }, + { replaceDescriptor: true, name: "rejects a replacement descriptor before permission" }, + ])("$name", { timeout: 180_000 }, async ({ replaceDescriptor }) => { + execFileSync("git", ["init", "--quiet", workDir], { windowsHide: true }); + const fixture = createLegacySseServer(replaceDescriptor); + const client = new CopilotClient({ + workingDirectory: workDir, + gitHubToken: harnessEnv.GITHUB_TOKEN, + connection: RuntimeConnection.forStdio({ path: process.env.COPILOT_CLI_PATH }), + env: { + ...harnessEnv, + TOOL_SEARCH_DISABLED: "1", + COPILOT_MCP_TOOL_CACHE: "true", + COPILOT_CACHE_HOME: join(workDir, "cache"), + COPILOT_MCP_APPS: "false", + COPILOT_DISABLE_KEYTAR: "1", + }, + }); + let starting: Promise | undefined; + onTestFinished(async () => { + const failures: unknown[] = []; + try { + failures.push(...(await client.stop())); + } catch (error) { + failures.push(error); + } + await Promise.allSettled(starting ? [starting] : []); + try { + await fixture.close(); + fixture.assertHealthy(); + } catch (error) { + failures.push(error); + } + if (failures.length) { + throw new AggregateError(failures, "Legacy SSE test cleanup failed"); + } + }); + starting = fixture.start(); + const url = await starting; + const permissions: PermissionRequest[] = []; + const events: SessionEvent[] = []; + const session = await client.createSession({ + mcpServers: { probe: { type: "sse", url, tools: ["*"], timeout: ACK_TIMEOUT_MS } }, + onPermissionRequest: (request, invocation) => { + permissions.push(request); + return approveAll(request, invocation); + }, + }); + session.on((event) => events.push(event)); + + async function waitForFixture( + condition: () => boolean, + phase: string, + timeoutMs = STEP_TIMEOUT_MS + ) { + try { + await waitForCondition( + () => { + fixture.assertHealthy(); + return condition(); + }, + { + timeoutMs, + timeoutMessage: `Legacy SSE ${phase} did not complete`, + } + ); + } catch (error) { + const servers = await session.rpc.mcp.list().then( + (listing) => listing.servers, + (listingError: unknown) => ({ error: String(listingError) }) + ); + throw new Error( + `Legacy SSE ${phase}: ${JSON.stringify({ + initialized: fixture.initialized, + listed: fixture.listed, + calls: fixture.calls, + heldPost: fixture.heldPost, + closedStreams: fixture.closedStreams, + servers, + events: events.filter((event) => event.type.startsWith("session.mcp_")), + })}`, + { cause: error } + ); + } + } + + await session.rpc.tools.initializeAndValidate(); + await waitForFixture(() => fixture.listed.includes(1), "initial listing"); + expect( + (await session.rpc.tools.getCurrentMetadata()).tools.map((tool) => tool.name) + ).toContain("probe-ping"); + // Direct SDK execution drives the real offered-tool/permission pipeline without a model. + const initial = await session.rpc.tools.execute({ + name: "probe-ping", + arguments: { marker: "held" }, + toolCallId: "sse-held", + }); + expect(initial).toMatchObject({ + resultType: "success", + textResultForLlm: expect.stringContaining("SSE_REPLY_1_held"), + }); + const permissionsBeforeReconnect = permissions.length; + // A configured 2s deadline must not silently use the old 30s or default 180s deadline. + await waitForFixture( + () => fixture.heldPost?.closed === true && fixture.closedStreams.includes(1), + "POST cancellation and SSE closure", + 10_000 + ); + expect(fixture.heldPost).toEqual({ connection: 1, closed: true, headersSent: false }); + expect(fixture.progressSent).toBeGreaterThan(0); + await waitForFixture( + () => fixture.initialized.some((connection) => connection > 1), + "automatic reconnect" + ); + await waitForCondition( + async () => + (await session.rpc.mcp.list()).servers.some( + (server) => server.name === "probe" && server.status === "connected" + ), + { + timeoutMs: STEP_TIMEOUT_MS, + timeoutMessage: "Reconnected MCP server is not connected", + } + ); + await waitForCondition( + () => + events.some( + (event) => + event.type === "session.mcp_server_removed" && + event.data.serverName === "probe" + ), + { + timeoutMs: STEP_TIMEOUT_MS, + timeoutMessage: "Session did not observe transport closure", + } + ); + + if (replaceDescriptor) { + const stale = await session.rpc.tools.execute({ + name: "probe-ping", + arguments: { marker: "stale" }, + toolCallId: "sse-stale", + }); + expect(stale).toMatchObject({ + resultType: "failure", + error: expect.stringMatching(/catalog changed|descriptor.*changed/i), + }); + expect(permissions).toHaveLength(permissionsBeforeReconnect); + expect(fixture.calls.some((call) => call.marker === "stale")).toBe(false); + await session.rpc.tools.initializeAndValidate(); + } + const subsequent = await session.rpc.tools.execute({ + name: "probe-ping", + arguments: { marker: "next" }, + toolCallId: "sse-next", + }); + expect(subsequent).toMatchObject({ + resultType: "success", + textResultForLlm: expect.stringMatching(/SSE_REPLY_[2-9]\d*_next/), + }); + if (replaceDescriptor) { + expect(permissions.slice(permissionsBeforeReconnect)).toEqual([ + expect.objectContaining({ + kind: "mcp", + serverName: "probe", + toolName: "probe-ping", + toolCallId: "sse-next", + readOnly: false, + args: { marker: "next" }, + }), + ]); + } + expect(fixture.calls.filter((call) => call.marker === "held")).toEqual([ + { connection: 1, id: fixture.calls[0].id, marker: "held" }, + ]); + expect(fixture.calls.filter((call) => call.marker === "next")).toHaveLength(1); + fixture.assertHealthy(); + }); +}); diff --git a/nodejs/test/e2e/mode_handlers.e2e.test.ts b/nodejs/test/e2e/mode_handlers.e2e.test.ts index 71c4b08963..b178523cd0 100644 --- a/nodejs/test/e2e/mode_handlers.e2e.test.ts +++ b/nodejs/test/e2e/mode_handlers.e2e.test.ts @@ -2,16 +2,21 @@ * Copyright (c) Microsoft Corporation. All rights reserved. *--------------------------------------------------------------------------------------------*/ -import { describe, expect, it } from "vitest"; +import { createServer } from "node:http"; +import { text } from "node:stream/consumers"; +import { describe, expect, it, onTestFinished } from "vitest"; import type { AutoModeSwitchRequest, CopilotSession, ExitPlanModeRequest, ExitPlanModeResult, + NamedProviderConfig, + ProviderModelConfig, SessionEvent, } from "../../src/index.js"; import { approveAll } from "../../src/index.js"; import { createSdkTestContext } from "./harness/sdkTestContext.js"; +import { waitForCondition } from "./harness/sdkTestHelper.js"; const EVENT_TIMEOUT_MS = 30_000; const MODE_HANDLER_TOKEN = "mode-handler-token"; @@ -20,6 +25,14 @@ const PLAN_PROMPT = "Create a brief implementation plan for adding a greeting.txt file, then request approval with exit_plan_mode."; const AUTO_MODE_PROMPT = "Explain that auto mode recovered from a rate limit in one short sentence."; +const PLAN_OVERRIDE_PROMPT = + "Write a plan, then call task_complete with summary PLAN_OVERRIDE_TOOL_CALLED."; +const INTERACTIVE_AFTER_PLAN_PROMPT = "Reply with exactly INTERACTIVE_AFTER_PLAN_OVERRIDE."; +const AUTOPILOT_READY_PROMPT = "Reply with exactly AUTOPILOT_MODE_READY."; +const AUTOPILOT_TO_INTERACTIVE_PROMPT = + "Call task_complete after the test switches this in-flight turn from Autopilot to Interactive."; +const INTERACTIVE_TO_AUTOPILOT_PROMPT = + "Call task_complete after the test switches this in-flight turn from Interactive to Autopilot."; function waitForEvent( session: CopilotSession, @@ -53,7 +66,7 @@ function waitForEvent( } describe("Mode handlers", async () => { - const { copilotClient: client, openAiEndpoint, env } = await createSdkTestContext(); + const { copilotClient: client, openAiEndpoint, env, workDir } = await createSdkTestContext(); env.COPILOT_DEBUG_GITHUB_API_URL = env.COPILOT_API_URL; await openAiEndpoint.setCopilotUserByToken(MODE_HANDLER_TOKEN, { @@ -126,6 +139,404 @@ describe("Mode handlers", async () => { } }); + it("handles Plan overrides and in-flight mode changes", { timeout: 180_000 }, async () => { + const modelRequests: Array<{ + messages?: Array<{ + role?: string; + content?: unknown; + tool_calls?: Array<{ function?: { name?: string } }>; + }>; + tools?: Array<{ function?: { name?: string } }>; + }> = []; + const autopilotToInteractiveGate = Promise.withResolvers(); + const interactiveToAutopilotGate = Promise.withResolvers(); + let modelFailure: Error | undefined; + const modelServer = createServer((request, response) => { + void (async () => { + const modelRequest = JSON.parse(await text(request)) as { + messages?: Array<{ + role?: string; + content?: unknown; + tool_calls?: Array<{ function?: { name?: string } }>; + }>; + tools?: Array<{ function?: { name?: string } }>; + }; + modelRequests.push(modelRequest); + const messages = modelRequest.messages ?? []; + const latestUserIndex = messages.findLastIndex( + (message) => message.role === "user" + ); + const activeMessages = messages.slice(Math.max(0, latestUserIndex)); + const latestUserContent = JSON.stringify(messages[latestUserIndex]?.content ?? ""); + const calledTools = + activeMessages.flatMap( + (message) => + message.tool_calls + ?.map((toolCall) => toolCall.function?.name) + .filter((name): name is string => name !== undefined) ?? [] + ) ?? []; + let message; + if (latestUserContent.includes(INTERACTIVE_AFTER_PLAN_PROMPT)) { + message = { role: "assistant", content: "INTERACTIVE_AFTER_PLAN_OVERRIDE" }; + } else if (latestUserContent.includes(AUTOPILOT_READY_PROMPT)) { + if (calledTools.includes("task_complete")) { + message = { + role: "assistant", + content: "UNEXPECTED_AUTOPILOT_READY_CONTINUATION", + }; + } else { + message = { + role: "assistant", + content: null, + tool_calls: [ + { + id: "autopilot-ready-task-complete", + type: "function", + function: { + name: "task_complete", + arguments: '{"summary":"AUTOPILOT_MODE_READY"}', + }, + }, + ], + }; + } + } else if (latestUserContent.includes(AUTOPILOT_TO_INTERACTIVE_PROMPT)) { + if (calledTools.includes("task_complete")) { + message = { + role: "assistant", + content: "AUTOPILOT_TO_INTERACTIVE_CONTINUED", + }; + } else { + await autopilotToInteractiveGate.promise; + message = { + role: "assistant", + content: null, + tool_calls: [ + { + id: "autopilot-to-interactive-task-complete", + type: "function", + function: { + name: "task_complete", + arguments: '{"summary":"MODE_SWITCHED_TO_INTERACTIVE"}', + }, + }, + ], + }; + } + } else if (latestUserContent.includes(INTERACTIVE_TO_AUTOPILOT_PROMPT)) { + if (calledTools.includes("task_complete")) { + message = { + role: "assistant", + content: "UNEXPECTED_INTERACTIVE_TO_AUTOPILOT_CONTINUATION", + }; + } else { + await interactiveToAutopilotGate.promise; + message = { + role: "assistant", + content: null, + tool_calls: [ + { + id: "interactive-to-autopilot-task-complete", + type: "function", + function: { + name: "task_complete", + arguments: '{"summary":"MODE_SWITCHED_TO_AUTOPILOT"}', + }, + }, + ], + }; + } + } else if (calledTools.includes("task_complete")) { + message = { role: "assistant", content: "PLAN_OVERRIDE_CONTINUED" }; + } else { + message = { + role: "assistant", + content: null, + tool_calls: [ + { + id: "plan-override-task-complete", + type: "function", + function: { + name: "task_complete", + arguments: '{"summary":"PLAN_OVERRIDE_TOOL_CALLED"}', + }, + }, + ], + }; + } + response.writeHead(200, { "content-type": "application/json" }); + response.end( + JSON.stringify({ + id: `plan-override-${modelRequests.length}`, + object: "chat.completion", + created: 0, + model: "test-model", + choices: [ + { + index: 0, + message, + finish_reason: "tool_calls" in message ? "tool_calls" : "stop", + }, + ], + usage: { prompt_tokens: 10, completion_tokens: 10, total_tokens: 20 }, + }) + ); + })().catch((error: unknown) => { + modelFailure = error instanceof Error ? error : new Error(String(error)); + response.writeHead(500).end(); + }); + }); + let session: CopilotSession | undefined; + const sessions: CopilotSession[] = []; + let serverStart: Promise | undefined; + onTestFinished(async () => { + const errors: unknown[] = []; + for (const activeSession of sessions.toReversed()) { + try { + await activeSession.disconnect(); + } catch (error) { + errors.push(error); + } + } + if (serverStart) { + const [startup] = await Promise.allSettled([serverStart]); + if (startup.status === "rejected") { + errors.push(startup.reason); + } + } + modelServer.closeAllConnections(); + if (modelServer.listening) { + try { + await new Promise((resolve, reject) => { + modelServer.close((error) => (error ? reject(error) : resolve())); + }); + } catch (error) { + errors.push(error); + } + } + if (errors.length) { + throw new AggregateError( + errors, + "Per-message Plan override fixture cleanup failed" + ); + } + }); + serverStart = new Promise((resolve, reject) => { + modelServer.once("error", reject); + modelServer.listen(0, "127.0.0.1", resolve); + }); + await serverStart; + const address = modelServer.address(); + if (!address || typeof address === "string") { + throw new Error("Missing local model server address"); + } + + const providers: NamedProviderConfig[] = [ + { + name: "local", + type: "openai", + baseUrl: `http://127.0.0.1:${address.port}`, + apiKey: "test", + wireApi: "completions", + }, + ]; + const models: ProviderModelConfig[] = [ + { id: "model", provider: "local", modelId: "test-model", wireModel: "test-model" }, + ]; + session = await client.createSession({ + onPermissionRequest: approveAll, + workingDirectory: workDir, + providers, + models, + model: "local/model", + }); + sessions.push(session); + + const planResponse = await session.sendAndWait({ + prompt: PLAN_OVERRIDE_PROMPT, + agentMode: "plan", + }); + const interactiveResponse = await session.sendAndWait({ + prompt: INTERACTIVE_AFTER_PLAN_PROMPT, + }); + await waitForCondition(() => modelRequests.length >= 3, { + timeoutMs: EVENT_TIMEOUT_MS, + timeoutMessage: + "The local model did not receive the Plan, continuation, and Interactive requests", + }); + + if (modelFailure) { + throw modelFailure; + } + expect(modelRequests).toHaveLength(3); + const firstSystemMessages = modelRequests[0].messages?.filter( + (message) => message.role === "system" + ); + const firstUserMessages = modelRequests[0].messages?.filter( + (message) => message.role === "user" + ); + expect(JSON.stringify(firstSystemMessages)).not.toContain(""); + expect(JSON.stringify(firstUserMessages)).toContain(""); + expect(JSON.stringify(firstUserMessages)).toContain(""); + expect( + modelRequests[0].tools?.some((tool) => tool.function?.name === "task_complete") + ).toBe(true); + expect(JSON.stringify(modelRequests[1].messages)).toContain("Continue working"); + expect(JSON.stringify(modelRequests[2].messages)).toContain( + "Plan mode is no longer active." + ); + expect(planResponse?.data.content).toBe("PLAN_OVERRIDE_CONTINUED"); + expect(interactiveResponse?.data.content).toBe("INTERACTIVE_AFTER_PLAN_OVERRIDE"); + await session.disconnect(); + sessions.splice(sessions.indexOf(session), 1); + + const approvalSession = await client.createSession({ + onPermissionRequest: approveAll, + workingDirectory: workDir, + providers, + models, + model: "local/model", + }); + sessions.push(approvalSession); + const enteredAutopilot = waitForEvent( + approvalSession, + (event): event is Extract => + event.type === "session.mode_changed" && event.data.newMode === "autopilot", + "session.mode_changed event for Autopilot warm-up" + ); + await approvalSession.rpc.mode.set({ mode: "autopilot" }); + await enteredAutopilot; + let autopilotReadyState: "pending" | "idle" | "error" = "pending"; + let autopilotReadyError: Error | undefined; + const unsubscribeAutopilotReady = approvalSession.on((event) => { + if (event.type === "session.idle") { + autopilotReadyState = "idle"; + } else if (event.type === "session.error") { + autopilotReadyState = "error"; + autopilotReadyError = new Error(`${event.data.message}\n${event.data.stack ?? ""}`); + } + }); + try { + await approvalSession.send({ prompt: AUTOPILOT_READY_PROMPT }); + await waitForCondition( + () => autopilotReadyState !== "pending" || modelRequests.length >= 5, + { + timeoutMs: EVENT_TIMEOUT_MS, + timeoutMessage: "The Autopilot warm-up turn did not finish or continue", + } + ); + } finally { + unsubscribeAutopilotReady(); + } + if (autopilotReadyError) { + throw autopilotReadyError; + } + expect(autopilotReadyState).toBe("idle"); + expect(modelRequests).toHaveLength(4); + expect(JSON.stringify(modelRequests)).not.toContain( + "UNEXPECTED_AUTOPILOT_READY_CONTINUATION" + ); + + let exitAutopilotState: "pending" | "idle" | "error" = "pending"; + let exitAutopilotError: Error | undefined; + const unsubscribeExitAutopilot = approvalSession.on((event) => { + if (event.type === "session.idle") { + exitAutopilotState = "idle"; + } else if (event.type === "session.error") { + exitAutopilotState = "error"; + exitAutopilotError = new Error(`${event.data.message}\n${event.data.stack ?? ""}`); + } + }); + try { + await approvalSession.send({ prompt: AUTOPILOT_TO_INTERACTIVE_PROMPT }); + await waitForCondition(() => modelRequests.length >= 5, { + timeoutMs: EVENT_TIMEOUT_MS, + timeoutMessage: "The Autopilot request did not reach the local model", + }); + const enteredInteractive = waitForEvent( + approvalSession, + (event): event is Extract => + event.type === "session.mode_changed" && event.data.newMode === "interactive", + "session.mode_changed event for Autopilot to Interactive" + ); + await approvalSession.rpc.mode.set({ mode: "interactive" }); + await enteredInteractive; + autopilotToInteractiveGate.resolve(); + await waitForCondition( + () => exitAutopilotState !== "pending" || modelRequests.length >= 6, + { + timeoutMs: EVENT_TIMEOUT_MS, + timeoutMessage: "The Autopilot-to-Interactive turn did not finish or continue", + } + ); + if (exitAutopilotState === "pending") { + await waitForCondition(() => exitAutopilotState !== "pending", { + timeoutMs: EVENT_TIMEOUT_MS, + timeoutMessage: "The Autopilot-to-Interactive continuation did not reach idle", + }); + } + } finally { + unsubscribeExitAutopilot(); + } + if (exitAutopilotError) { + throw exitAutopilotError; + } + expect(exitAutopilotState).toBe("idle"); + expect(modelRequests.length).toBeGreaterThanOrEqual(6); + expect( + modelRequests + .slice(5) + .some((request) => JSON.stringify(request.messages).includes("Continue working")) + ).toBe(true); + + const beforeEnterAutopilot = modelRequests.length; + let enterAutopilotState: "pending" | "idle" | "error" = "pending"; + let enterAutopilotError: Error | undefined; + const unsubscribeEnterAutopilot = approvalSession.on((event) => { + if (event.type === "session.idle") { + enterAutopilotState = "idle"; + } else if (event.type === "session.error") { + enterAutopilotState = "error"; + enterAutopilotError = new Error(`${event.data.message}\n${event.data.stack ?? ""}`); + } + }); + try { + await approvalSession.send({ prompt: INTERACTIVE_TO_AUTOPILOT_PROMPT }); + await waitForCondition(() => modelRequests.length >= beforeEnterAutopilot + 1, { + timeoutMs: EVENT_TIMEOUT_MS, + timeoutMessage: "The Interactive request did not reach the local model", + }); + const reenteredAutopilot = waitForEvent( + approvalSession, + (event): event is Extract => + event.type === "session.mode_changed" && event.data.newMode === "autopilot", + "session.mode_changed event for Interactive to Autopilot" + ); + await approvalSession.rpc.mode.set({ mode: "autopilot" }); + await reenteredAutopilot; + interactiveToAutopilotGate.resolve(); + await waitForCondition( + () => + enterAutopilotState !== "pending" || + modelRequests.length >= beforeEnterAutopilot + 2, + { + timeoutMs: EVENT_TIMEOUT_MS, + timeoutMessage: "The Interactive-to-Autopilot turn did not finish or continue", + } + ); + } finally { + unsubscribeEnterAutopilot(); + } + if (enterAutopilotError) { + throw enterAutopilotError; + } + expect(enterAutopilotState).toBe("idle"); + expect(modelRequests).toHaveLength(beforeEnterAutopilot + 1); + expect(JSON.stringify(modelRequests)).not.toContain( + "UNEXPECTED_INTERACTIVE_TO_AUTOPILOT_CONTINUATION" + ); + }); + it("should invoke auto mode switch handler when rate limited", async () => { const autoModeSwitchRequests: AutoModeSwitchRequest[] = []; let session: CopilotSession | undefined; diff --git a/nodejs/test/e2e/model_effort_resume.e2e.test.ts b/nodejs/test/e2e/model_effort_resume.e2e.test.ts new file mode 100644 index 0000000000..d458118761 --- /dev/null +++ b/nodejs/test/e2e/model_effort_resume.e2e.test.ts @@ -0,0 +1,221 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +import { writeFile } from "node:fs/promises"; +import { join } from "node:path"; +import { describe, expect, it, onTestFinished } from "vitest"; +import { approveAll, RuntimeConnection } from "../../src/index.js"; +import type { NamedProviderConfig, ProviderModelConfig } from "../../src/index.js"; +import { createSdkTestContext } from "./harness/sdkTestContext.js"; + +describe("Model-bound effort across runtime restart", async () => { + const { createClient, openAiEndpoint, env } = await createSdkTestContext(); + + it.each([ + { override: undefined, replacement: "switch", plan: false }, + { override: undefined, replacement: "policy", plan: false }, + { override: "rpc", replacement: "policy", plan: false }, + { override: "options", replacement: "switch", plan: false }, + { override: "options", replacement: "options", plan: false }, + { override: "combined", replacement: "switch", plan: false }, + { override: "model-only", replacement: "options", plan: false }, + { override: undefined, replacement: "switch", plan: true }, + { override: "rpc", replacement: "switch", plan: true }, + ] as const)( + "restores ownership with $override effort override, $replacement replacement, and plan=$plan", + async ({ override, replacement, plan }) => { + const providers: NamedProviderConfig[] = [ + { + name: "effort-resume", + type: "openai", + wireApi: "completions", + baseUrl: openAiEndpoint.url, + }, + ]; + const models: ProviderModelConfig[] = [ + { id: "bound", provider: "effort-resume", modelId: "gpt-5.4-mini" }, + { id: "replacement", provider: "effort-resume", modelId: "gpt-5.4-mini" }, + ]; + const customAgents = [ + { + name: "cold-resume-agent", + prompt: "Respond concisely.", + model: "effort-resume/bound:defaultReasoningEffort=high", + }, + ]; + // Pin child-process transport: recreating an in-process client is not a cold restart. + const originalClient = createClient({ connection: RuntimeConnection.forStdio() }); + onTestFinished(async () => { + expect(await originalClient.stop()).toHaveLength(0); + }); + const fixtureSession = await originalClient.createSession({ + onPermissionRequest: approveAll, + model: "effort-resume/bound", + providers, + models, + customAgents, + }); + // Create workspace metadata without a model turn; this does not enable journal persistence. + await fixtureSession.rpc.name.set({ + name: `effort-resume-${fixtureSession.sessionId}`, + }); + await fixtureSession.rpc.agent.select({ name: "cold-resume-agent" }); + await fixtureSession.rpc.model.switchTo({ + modelId: "effort-resume/bound", + reasoningEffort: "high", + source: "agent", + }); + const fixtureEvents = await fixtureSession.getEvents(); + expect(fixtureEvents[0]?.type).toBe("session.start"); + const transported = fixtureEvents + .filter((event) => event.type === "session.model_change") + .at(-1); + expect(transported?.data.reasoningEffort).toBe("high"); + expect(transported?.data.source).toBe("sdk"); + expect(transported?.data.reasoningEffortModel).toBeUndefined(); + await originalClient.rpc.sessions.save({ sessionId: fixtureSession.sessionId }); + await originalClient.rpc.sessions.close({ sessionId: fixtureSession.sessionId }); + expect(await originalClient.stop()).toHaveLength(0); + + // SDK callers cannot claim agent provenance, and a no-turn session has no journal. + // Seed its emitted history and agent receipt only after the writer stops. + const eventsPath = join( + env.COPILOT_HOME, + "session-state", + fixtureSession.sessionId, + "events.jsonl" + ); + const authored = fixtureEvents + .filter((event) => event.type === "session.model_change") + .at(-1); + if (!authored) { + throw new Error("The runtime did not emit the model switch fixture"); + } + expect(authored.data.reasoningEffort).toBe("high"); + authored.data.source = "agent"; + authored.data.reasoningEffortModel = "effort-resume/bound"; + await writeFile( + eventsPath, + `${fixtureEvents.map((event) => JSON.stringify(event)).join("\n")}\n` + ); + + const boundClient = createClient({ connection: RuntimeConnection.forStdio() }); + onTestFinished(async () => { + expect(await boundClient.stop()).toHaveLength(0); + }); + const session = await boundClient.resumeSession(fixtureSession.sessionId, { + onPermissionRequest: approveAll, + providers, + models, + customAgents, + }); + const bound = (await session.getEvents()) + .filter((event) => event.type === "session.model_change") + .at(-1); + expect(bound?.data.reasoningEffortModel).toBe("effort-resume/bound"); + if (override === "rpc") { + await session.rpc.model.setReasoningEffort({ reasoningEffort: "high" }); + } else if (override === "options") { + await session.rpc.options.update({ reasoningEffort: "high" }); + } else if (override === "combined") { + await session.rpc.options.update({ + model: "effort-resume/replacement", + reasoningEffort: "high", + }); + } else if (override === "model-only") { + await session.rpc.options.update({ model: "effort-resume/bound" }); + expect((await session.rpc.model.getCurrent()).reasoningEffort).toBe("high"); + const unchanged = (await session.getEvents()) + .filter((event) => event.type === "session.model_change") + .at(-1); + expect(unchanged?.data.reasoningEffortModel).toBe("effort-resume/bound"); + await session.rpc.options.update({ model: "effort-resume/replacement" }); + } + const updatedModel = + override === "combined" || override === "model-only" + ? "effort-resume/replacement" + : "effort-resume/bound"; + const expectedEffort = override === "model-only" ? undefined : "high"; + const updatedControls = await session.rpc.model.getCurrent(); + expect(updatedControls.modelId).toBe(updatedModel); + expect(updatedControls.reasoningEffort).toBe(expectedEffort); + const beforeRestart = (await session.getEvents()) + .filter((event) => event.type === "session.model_change") + .at(-1); + expect(beforeRestart?.data.reasoningEffort).toBe(expectedEffort ?? null); + expect(beforeRestart?.data.newModel).toBe(updatedModel); + expect(beforeRestart?.data.reasoningEffortModel).toBe( + override === undefined ? "effort-resume/bound" : undefined + ); + const planSelection = { + planModelConfigured: true, + planModel: "effort-resume/replacement", + planReasoningEffort: "low", + }; + if (plan) { + await session.rpc.mode.set({ mode: "plan", ...planSelection }); + expect(await session.rpc.mode.get()).toBe("plan"); + expect(await session.rpc.model.getCurrent()).toMatchObject({ + modelId: "effort-resume/replacement", + reasoningEffort: "low", + }); + } + await boundClient.rpc.sessions.save({ sessionId: session.sessionId }); + await boundClient.rpc.sessions.close({ sessionId: session.sessionId }); + expect(await boundClient.stop()).toHaveLength(0); + + const resumedClient = createClient({ connection: RuntimeConnection.forStdio() }); + onTestFinished(async () => { + expect(await resumedClient.stop()).toHaveLength(0); + }); + const resumed = await resumedClient.resumeSession(session.sessionId, { + onPermissionRequest: approveAll, + providers, + models, + customAgents, + }); + if (plan) { + expect(await resumed.rpc.mode.get()).toBe("plan"); + await resumed.rpc.mode.set({ mode: "interactive", ...planSelection }); + expect(await resumed.rpc.mode.get()).toBe("interactive"); + } + const restoredControls = await resumed.rpc.model.getCurrent(); + expect(restoredControls.modelId).toBe(updatedModel); + expect(restoredControls.reasoningEffort).toBe(expectedEffort); + if (override === "model-only") { + await resumed.rpc.options.update({ model: "effort-resume/bound" }); + const revertedControls = await resumed.rpc.model.getCurrent(); + expect(revertedControls.modelId).toBe("effort-resume/bound"); + expect(revertedControls.reasoningEffort).toBeUndefined(); + } + if (replacement === "policy") { + await resumed.rpc.model.setAllowedModels({ + allowedModels: ["effort-resume/replacement"], + }); + } else if (replacement === "options") { + await resumed.rpc.options.update({ model: "effort-resume/replacement" }); + } else { + await resumed.rpc.model.switchTo({ modelId: "effort-resume/replacement" }); + } + const replacementEffort = + override === undefined || override === "model-only" ? undefined : "high"; + const replacedControls = await resumed.rpc.model.getCurrent(); + expect(replacedControls.modelId).toBe("effort-resume/replacement"); + expect(replacedControls.reasoningEffort).toBe(replacementEffort); + const replaced = (await resumed.getEvents()) + .filter((event) => event.type === "session.model_change") + .at(-1); + expect(replaced?.data.newModel).toBe("effort-resume/replacement"); + expect(replaced?.data.reasoningEffort).toBe(replacementEffort ?? null); + expect(replaced?.data.reasoningEffortModel).toBeUndefined(); + await resumedClient.rpc.sessions.close({ sessionId: resumed.sessionId }); + expect(await resumedClient.stop()).toHaveLength(0); + expect( + (await openAiEndpoint.getRequests()).filter((request) => + /\/(?:completions|responses|messages)(?:\?|$)/.test(request.url) + ) + ).toHaveLength(0); + } + ); +}); diff --git a/nodejs/test/e2e/multi-client.e2e.test.ts b/nodejs/test/e2e/multi-client.e2e.test.ts index 79bd0e8c49..f0f7a6c33e 100644 --- a/nodejs/test/e2e/multi-client.e2e.test.ts +++ b/nodejs/test/e2e/multi-client.e2e.test.ts @@ -4,7 +4,7 @@ import { describe, expect, it, afterAll } from "vitest"; import { z } from "zod"; -import { CopilotClient, defineTool, approveAll, RuntimeConnection } from "../../src/index.js"; +import { defineTool, approveAll, RuntimeConnection } from "../../src/index.js"; import type { SessionEvent } from "../../src/index.js"; import { createSdkTestContext, isInProcessTransport } from "./harness/sdkTestContext"; @@ -24,7 +24,7 @@ describe("Multi-client broadcast", async () => { await initSession.disconnect(); const runtimePort = (client1 as unknown as { runtimePort: number }).runtimePort; - let client2 = new CopilotClient({ + let client2 = ctx.createClient({ connection: RuntimeConnection.forUri(`localhost:${runtimePort}`, { connectionToken: tcpConnectionToken, }), @@ -360,7 +360,7 @@ describe("Multi-client broadcast", async () => { process.removeListener("unhandledRejection", suppressDisposed); // Recreate client2 for cleanup in afterAll (but don't rejoin the session) - client2 = new CopilotClient({ + client2 = ctx.createClient({ connection: RuntimeConnection.forUri(`localhost:${runtimePort}`, { connectionToken: tcpConnectionToken, }), diff --git a/nodejs/test/e2e/pending_work_resume.e2e.test.ts b/nodejs/test/e2e/pending_work_resume.e2e.test.ts index 0f4fd4a74f..65ab9fccc9 100644 --- a/nodejs/test/e2e/pending_work_resume.e2e.test.ts +++ b/nodejs/test/e2e/pending_work_resume.e2e.test.ts @@ -140,11 +140,11 @@ function waitForPermissionRequest(session: CopilotSession): Promise { - const { env, workDir, openAiEndpoint } = await createSdkTestContext(); + const { env, workDir, openAiEndpoint, createClient } = await createSdkTestContext(); const SHARED_TOKEN = "pending-work-resume-shared-test-token"; function createTcpServer(): CopilotClient { - const server = new CopilotClient({ + const server = createClient({ workingDirectory: workDir, env, gitHubToken: DEFAULT_GITHUB_TOKEN, @@ -164,7 +164,7 @@ describe("Pending work resume", async () => { } function createConnectingClient(cliUrl: string): CopilotClient { - const client = new CopilotClient({ + const client = createClient({ connection: RuntimeConnection.forUri(cliUrl, { connectionToken: SHARED_TOKEN }), }); onTestFinished(async () => { @@ -444,7 +444,21 @@ describe("Pending work resume", async () => { requestId: toolB.data.requestId, result: "PARALLEL_B_BETA", }); - expect(resultB.success).toBe(true); + expect( + resultB.success, + resultB.success + ? undefined + : `Rejected request ${toolB.data.requestId}; resumed events: ${JSON.stringify( + ( + await session2.rpc.eventLog.read({ max: 100, waitMs: 0 }) + ).events.filter( + (event) => + event.type.startsWith("external_tool.") || + event.type === "tool.execution_complete" || + event.type === "session.resume" + ) + )}` + ).toBe(true); const resultA = await session2.rpc.tools.handlePendingToolCall({ requestId: toolA.data.requestId, result: "PARALLEL_A_ALPHA", diff --git a/nodejs/test/e2e/permissions.e2e.test.ts b/nodejs/test/e2e/permissions.e2e.test.ts index 565719e0a4..e9ed80d5d0 100644 --- a/nodejs/test/e2e/permissions.e2e.test.ts +++ b/nodejs/test/e2e/permissions.e2e.test.ts @@ -5,7 +5,7 @@ import { realpathSync } from "fs"; import { mkdir, readFile, writeFile } from "fs/promises"; import { join } from "path"; -import { describe, expect, it, vi } from "vitest"; +import { describe, expect, it, onTestFailed, vi } from "vitest"; import { z } from "zod"; import type { PermissionDecisionContext, @@ -237,13 +237,19 @@ describe("Permission callbacks", async () => { it("should resume session with permission handler", async () => { const permissionRequests: PermissionRequest[] = []; + let phase = "creating the initial session"; + onTestFailed(() => { + console.error(`Permission resume test failed while ${phase}`); + }); // Create initial session const session1 = await client.createSession({ onPermissionRequest: approveAll }); const sessionId = session1.sessionId; + phase = "sending the initial prompt"; await session1.sendAndWait({ prompt: "What is 1+1?" }); // Resume with permission handler + phase = "resuming the session"; const session2 = await client.resumeSession(sessionId, { onPermissionRequest: (request) => { permissionRequests.push(request); @@ -251,13 +257,16 @@ describe("Permission callbacks", async () => { }, }); + phase = "sending the resumed prompt"; await session2.sendAndWait({ prompt: "Run 'echo resumed' for me", }); // Should have permission requests from resumed session + phase = "verifying permission requests"; expect(permissionRequests.length).toBeGreaterThan(0); + phase = "disconnecting the resumed session"; await session2.disconnect(); }); diff --git a/nodejs/test/e2e/rpc_session_state_extras.e2e.test.ts b/nodejs/test/e2e/rpc_session_state_extras.e2e.test.ts index 7b88af7e2d..a7e62c185f 100644 --- a/nodejs/test/e2e/rpc_session_state_extras.e2e.test.ts +++ b/nodejs/test/e2e/rpc_session_state_extras.e2e.test.ts @@ -6,6 +6,9 @@ import { describe, expect, it } from "vitest"; import type { CopilotSession } from "../../src/index.js"; import { approveAll, CopilotClient, RuntimeConnection } from "../../src/index.js"; import { createSdkTestContext, DEFAULT_GITHUB_TOKEN } from "./harness/sdkTestContext.js"; +import { isByokBackend } from "./harness/testBackend"; + +const capiIt = it.skipIf(isByokBackend); describe("Session-scoped state extras RPC", async () => { const { copilotClient: client, env, openAiEndpoint, workDir } = await createSdkTestContext(); @@ -103,7 +106,8 @@ describe("Session-scoped state extras RPC", async () => { } }); - it("should add byok provider and model at runtime", { timeout: 120_000 }, async () => { + // Owns a multi-provider registry rather than the matrix's single provider. + capiIt("should add byok provider and model at runtime", { timeout: 120_000 }, async () => { const session = await createSession(); try { const providerName = `sdk-runtime-provider-${Date.now()}-${Math.random().toString(36).slice(2)}`; diff --git a/nodejs/test/e2e/rpc_shell_user_requested.e2e.test.ts b/nodejs/test/e2e/rpc_shell_user_requested.e2e.test.ts index 73e8ac6de3..b8e3394313 100644 --- a/nodejs/test/e2e/rpc_shell_user_requested.e2e.test.ts +++ b/nodejs/test/e2e/rpc_shell_user_requested.e2e.test.ts @@ -11,7 +11,7 @@ import { createSdkTestContext } from "./harness/sdkTestContext.js"; import { waitForCondition } from "./harness/sdkTestHelper.js"; describe("User-requested shell RPC", async () => { - const { copilotClient: client, homeDir } = await createSdkTestContext(); + const { copilotClient: client, homeDir, workDir } = await createSdkTestContext(); function compactUuid(): string { return randomUUID().replace(/-/g, ""); @@ -177,6 +177,125 @@ describe("User-requested shell RPC", async () => { } ); + it( + "streams separate stdout and stderr before a controlled exit and never replays chunks", + { + timeout: 120_000, + }, + async () => { + const session = await client.createSession({ + onPermissionRequest: approveAll, + workingDirectory: workDir, + }); + const requestId = `stream-${compactUuid()}`; + const releasePath = join(workDir, "release"); + const events: SessionEvent[] = []; + let stdout = ""; + let stderr = ""; + let released = false; + let settled = false; + const unsubscribe = session.on((event) => { + events.push(event); + if (event.type !== "tool.shell_output") { + return; + } + if ((event.data.stream ?? "stdout") === "stdout") { + stdout += event.data.text; + } else if (event.data.stream === "stderr") { + stderr += event.data.text; + } + if ( + !released && + stdout.includes("shell-stream-live-out") && + stderr.includes("shell-stream-live-err") + ) { + released = true; + writeFileSync(releasePath, ""); + } + }); + // The public shell RPC exercises live delivery without a model choosing + // a tool or requiring a synthesized model response. + const execution = session.rpc.shell + .executeUserRequested({ + requestId, + command: `node -e "const fs=require('fs'),newline=String.fromCharCode(10);fs.watch('.',()=>{if(fs.existsSync('release')){fs.writeSync(1,'shell-stream-tail'+newline);process.exit(0)}});fs.writeSync(1,'shell-stream-live-out'+newline);fs.writeSync(2,'shell-stream-live-err'+newline)"`, + }) + .finally(() => { + settled = true; + }); + try { + const result = await withTimeout( + execution, + 75_000, + "Timed out waiting for live stdout/stderr to release the shell command." + ); + expect(result.success).toBe(true); + expect(result.exitCode).toBe(0); + expect(released).toBe(true); + expect(stdout).toBe("shell-stream-live-out\nshell-stream-tail\n"); + expect(stderr).toBe("shell-stream-live-err\n"); + const chunks = events.filter((event) => event.type === "tool.shell_output"); + expect(chunks.map((event) => event.data.sequence)).toEqual( + chunks.map((_, index) => index) + ); + expect(chunks.every((event) => event.ephemeral === true)).toBe(true); + expect(chunks.every((event) => event.data.toolCallId === result.toolCallId)).toBe( + true + ); + expect(result.toolCallId).toBeTruthy(); + const completionIndex = events.findIndex( + (event) => + event.type === "tool.execution_complete" && + event.data.toolCallId === result.toolCallId + ); + expect(completionIndex).toBeGreaterThan(0); + expect( + events + .slice(completionIndex) + .some( + (event) => + event.type === "tool.shell_output" && + event.data.toolCallId === result.toolCallId + ) + ).toBe(false); + expect( + events.some( + (event) => + event.type === "tool.execution_partial_result" && + event.data.toolCallId === result.toolCallId + ) + ).toBe(true); + let history: SessionEvent[] = []; + await waitForCondition( + async () => { + history = await session.getEvents(); + return history.some( + (event) => + event.type === "tool.execution_complete" && + event.data.toolCallId === result.toolCallId + ); + }, + { timeoutMessage: "Shell completion did not appear in session history." } + ); + expect(history.some((event) => event.type === "tool.shell_output")).toBe(false); + } finally { + unsubscribe(); + try { + if (!settled) { + await session.rpc.shell.cancelUserRequested({ requestId }); + await withTimeout( + execution, + 30_000, + "Timed out draining the streaming shell command." + ); + } + } finally { + await session.disconnect(); + } + } + } + ); + it("should cancel user requested shell command", { timeout: 120_000 }, async () => { const session = await client.createSession({ onPermissionRequest: approveAll }); const markerPath = join(homeDir, `shell-cancel-${compactUuid()}.txt`); diff --git a/nodejs/test/e2e/rpc_surface_coverage.e2e.test.ts b/nodejs/test/e2e/rpc_surface_coverage.e2e.test.ts index ce10918089..2a8f34a7d2 100644 --- a/nodejs/test/e2e/rpc_surface_coverage.e2e.test.ts +++ b/nodejs/test/e2e/rpc_surface_coverage.e2e.test.ts @@ -102,6 +102,20 @@ function handle(message) { return; } + if (message.method === "session.instructions.reload") { + writeResponse(message.id, { sources: [] }); + return; + } + + if (message.method === "session.customizations.reload") { + writeResponse(message.id, { + warnings: [], + errors: [], + outcomes: [{ subsystem: "instructions", status: "reloaded" }] + }); + return; + } + writeResponse(message.id, { method: message.method, params: message.params ?? null, @@ -313,7 +327,21 @@ describe("Generated RPC surface coverage", () => { ...collectRuntimeFunctions(session.rpc, "session"), ]); - expect(inventory).toHaveLength(384); + expect(inventory).toHaveLength(394); + const sessionProviderMethods = [ + "providers.getCatalog", + "providers.discover", + "providers.getStatus", + "providers.models.list", + "providers.models.prepareConfiguration", + ]; + for (const path of sessionProviderMethods) { + const wireMethod = `session.${path}`; + expect( + inventory.filter((method) => method.wireMethod === wireMethod), + `Missing generated session RPC ${wireMethod}` + ).toEqual([expect.objectContaining({ scope: "session", path })]); + } const boundInstallationMethods = [ "mcp.prepareInstall", "mcp.applyInstall", @@ -359,6 +387,19 @@ describe("Generated RPC surface coverage", () => { continue; } + if (method.wireMethod === "session.instructions.reload") { + expect(result).toEqual({ sources: [] }); + continue; + } + if (method.wireMethod === "session.customizations.reload") { + expect(result).toEqual({ + warnings: [], + errors: [], + outcomes: [{ subsystem: "instructions", status: "reloaded" }], + }); + continue; + } + expect(result).toMatchObject({ method: method.wireMethod, state: { @@ -402,6 +443,11 @@ describe("Generated RPC surface coverage", () => { await session.rpc.mode.set({ mode: "interactive" }); expect(await session.rpc.mode.get()).toBe("interactive"); + expect((await session.rpc.instructions.reload()).sources).toEqual([]); + expect((await session.rpc.customizations.reload()).outcomes).toEqual([ + { subsystem: "instructions", status: "reloaded" }, + ]); + await expect( client.rpc.ping({ message: "error", __forceError: true } as never) ).rejects.toMatchObject({ @@ -424,5 +470,11 @@ describe("Generated RPC surface coverage", () => { true ); } + for (const wireMethod of ["session.instructions.reload", "session.customizations.reload"]) { + expect(captured.filter((request) => request.method === wireMethod)).toEqual([ + { method: wireMethod, params: { sessionId: "rpc-surface-session" } }, + { method: wireMethod, params: { sessionId: "rpc-surface-session" } }, + ]); + } }); }); diff --git a/nodejs/test/e2e/rpc_tasks_and_handlers.e2e.test.ts b/nodejs/test/e2e/rpc_tasks_and_handlers.e2e.test.ts index 0071e1ff45..c2e62d3bcd 100644 --- a/nodejs/test/e2e/rpc_tasks_and_handlers.e2e.test.ts +++ b/nodejs/test/e2e/rpc_tasks_and_handlers.e2e.test.ts @@ -3,13 +3,53 @@ *--------------------------------------------------------------------------------------------*/ import { describe, expect, it } from "vitest"; +import { z } from "zod"; import { approveAll } from "../../src/index.js"; -import type { SessionEvent } from "../../src/index.js"; +import type { SessionEvent, CopilotSession } from "../../src/index.js"; import { createSdkTestContext } from "./harness/sdkTestContext.js"; import { waitForCondition } from "./harness/sdkTestHelper.js"; +import { isByokBackend } from "./harness/testBackend"; + +const capiIt = it.skipIf(isByokBackend); describe("Session tasks RPC and pending handlers", async () => { - const { copilotClient: client, openAiEndpoint } = await createSdkTestContext(); + const { + copilotClient: client, + openAiEndpoint, + createClient, + } = await createSdkTestContext({ + modelNames: { "claude-sonnet-5": "Claude Sonnet 5" }, + }); + + async function modelRequestsForPrompt(promptEnding: string) { + return (await openAiEndpoint.getRequests()) + .filter(({ method, url }) => method === "POST" && url.includes("/chat/completions")) + .map(({ body }) => + z + .object({ + messages: z.array(z.object({ role: z.string(), content: z.unknown() })), + model: z.string(), + reasoning_effort: z.string().optional(), + }) + .parse(JSON.parse(body)) + ) + .filter((request) => + request.messages.some( + (message) => + message.role === "user" && + typeof message.content === "string" && + message.content.trim().endsWith(promptEnding) + ) + ); + } + + async function getAgentTask(session: CopilotSession, agentId: string) { + const task = (await session.rpc.tasks.list()).tasks.find((entry) => entry.id === agentId); + if (task && task.type !== "agent") { + throw new Error(`Expected agent task ${agentId}, received ${JSON.stringify(task)}`); + } + return task; + } async function assertImplementedFailure( action: () => Promise, @@ -76,7 +116,8 @@ describe("Session tasks RPC and pending handlers", async () => { await session.disconnect(); }); - it("should report implemented error for invalid task agent model", async () => { + // BYOK model IDs are user-defined; only CAPI validates them against its catalog. + capiIt("should report implemented error for invalid task agent model", async () => { const session = await client.createSession({ onPermissionRequest: approveAll }); await assertImplementedFailure( @@ -132,8 +173,7 @@ describe("Session tasks RPC and pending handlers", async () => { name: "sdk-inherited-model-agent", description: "SDK inherited model coverage", }); - const findTask = async () => - (await session.rpc.tasks.list()).tasks.find((entry) => entry.id === agentId); + const findTask = () => getAgentTask(session, agentId); let lastTask: Awaited>; await waitForCondition( async () => { @@ -164,8 +204,8 @@ describe("Session tasks RPC and pending handlers", async () => { }); const task = await findTask(); - // No model was requested; the wire reports that as `null`. - expect(task?.model ?? null).toBeNull(); + // Provider-backed subagents carry an explicit model override; CAPI leaves it unset. + expect(task?.model ?? null).toBe(isByokBackend ? parentModel : null); expect(task?.resolvedModel).toBe(parentModel); expect(started.map((event) => event.data.model)).toEqual([parentModel]); // Raw requests, scoped by this test's prompts: an earlier test's late @@ -215,9 +255,7 @@ describe("Session tasks RPC and pending handlers", async () => { ), { timeoutMessage: `Background agent ${started.agentId} never appeared` } ); - const task = (await session.rpc.tasks.list()).tasks.find( - (entry) => entry.id === started.agentId - ); + const task = await getAgentTask(session, started.agentId); expect(task).toMatchObject({ id: started.agentId, agentType: "general-purpose", @@ -240,9 +278,7 @@ describe("Session tasks RPC and pending handlers", async () => { ); await waitForCondition( async () => { - const task = (await session.rpc.tasks.list()).tasks.find( - (entry) => entry.id === started.agentId - ); + const task = await getAgentTask(session, started.agentId); return ( !!task && ["completed", "idle"].includes(task.status) && @@ -251,9 +287,7 @@ describe("Session tasks RPC and pending handlers", async () => { }, { timeoutMs: 60_000, timeoutMessage: `Agent ${started.agentId} never settled` } ); - const current = (await session.rpc.tasks.list()).tasks.find( - (entry) => entry.id === started.agentId - ); + const current = await getAgentTask(session, started.agentId); expect(current?.latestResponse ?? current?.result).toContain("TASK_AGENT_DONE"); if (current?.status === "idle") { expect((await session.rpc.tasks.cancel({ id: started.agentId })).cancelled).toBe( @@ -275,6 +309,183 @@ describe("Session tasks RPC and pending handlers", async () => { } }); + capiIt( + "should apply a custom agent's composed model and effort when started over RPC", + { timeout: 240_000 }, + async () => { + const recordingClient = createClient(); + try { + const session = await recordingClient.createSession({ + onPermissionRequest: approveAll, + customAgents: [ + { + name: "sdk-composed-model-agent", + description: "SDK composed model coverage", + prompt: "Reply with SDK_COMPOSED_AGENT_DONE exactly.", + model: "claude-sonnet-5:defaultReasoningEffort=high", + }, + ], + }); + const configurations: Array<{ + agentId?: string; + model: string; + reasoningEffort?: string; + }> = []; + const unsubscribe = session.on((event) => { + if (event.type === "subagent.configured") { + configurations.push({ agentId: event.agentId, ...event.data }); + } + }); + try { + const started = await session.rpc.tasks.startAgent({ + agentType: "sdk-composed-model-agent", + prompt: "Reply with SDK_COMPOSED_AGENT_DONE exactly.", + name: "sdk-composed-model-task", + }); + await waitForCondition( + () => configurations.some((event) => event.agentId === started.agentId), + { + timeoutMs: 60_000, + timeoutMessage: `Agent ${started.agentId} was never configured: ${JSON.stringify(configurations)}`, + } + ); + expect( + configurations.find((event) => event.agentId === started.agentId) + ).toMatchObject({ + model: "claude-sonnet-5", + reasoningEffort: "high", + }); + + await waitForCondition( + async () => { + const task = await getAgentTask(session, started.agentId); + return ( + !!task && + ["completed", "idle"].includes(task.status) && + (task.latestResponse ?? task.result ?? "").includes( + "SDK_COMPOSED_AGENT_DONE" + ) + ); + }, + { + timeoutMs: 60_000, + timeoutMessage: `Agent ${started.agentId} never completed`, + } + ); + const requests = await modelRequestsForPrompt( + "Reply with SDK_COMPOSED_AGENT_DONE exactly." + ); + expect(requests.length).toBeGreaterThan(0); + for (const request of requests) { + expect(request).toMatchObject({ + model: "claude-sonnet-5", + reasoning_effort: "high", + }); + } + } finally { + unsubscribe(); + } + } finally { + await recordingClient.stop(); + } + } + ); + + capiIt( + "should preserve a composed display-name effort in a model-issued task", + async () => { + const taskArguments: unknown[] = []; + const recordingClient = createClient(); + try { + const session = await recordingClient.createSession({ + model: "claude-sonnet-5", + reasoningEffort: "medium", + onPermissionRequest: approveAll, + hooks: { + onPreToolUse: async (input) => { + if (input.toolName === "task") { + taskArguments.push(input.toolArgs); + } + return { permissionDecision: "allow" }; + }, + }, + customAgents: [ + { + name: "sdk-display-model-agent", + description: "SDK display-name model coverage", + prompt: "Reply with SDK_DISPLAY_AGENT_DONE exactly.", + model: "Claude Sonnet 5:defaultReasoningEffort=high", + }, + ], + }); + const modelList = await session.rpc.model.list(); + expect(modelList.list).toEqual( + expect.arrayContaining([ + expect.objectContaining({ id: "claude-sonnet-5", name: "Claude Sonnet 5" }), + ]) + ); + const configurations: Array<{ + agentId?: string; + model: string; + reasoningEffort?: string; + }> = []; + const unsubscribe = session.on((event) => { + if (event.type === "subagent.configured") { + configurations.push({ agentId: event.agentId, ...event.data }); + } + }); + try { + const reply = await session.sendAndWait( + { + prompt: + 'Use the task tool with agent_type "sdk-display-model-agent", mode "sync", and prompt "Reply with SDK_DISPLAY_AGENT_DONE exactly.". ' + + "Do not specify model or reasoning_effort in the task arguments. " + + "After it completes, reply with SDK_DISPLAY_PARENT_DONE exactly.", + }, + 120_000 + ); + expect(reply?.data.content).toContain("SDK_DISPLAY_PARENT_DONE"); + expect(taskArguments).toHaveLength(1); + expect(taskArguments[0]).toMatchObject({ + agent_type: "sdk-display-model-agent", + mode: "sync", + }); + expect(taskArguments[0]).not.toHaveProperty("model"); + expect(taskArguments[0]).not.toHaveProperty("reasoning_effort"); + expect(configurations).toHaveLength(1); + const configuration = configurations[0]; + expect(configuration).toMatchObject({ + model: "claude-sonnet-5", + reasoningEffort: "high", + }); + expect(configuration.agentId).toBeTruthy(); + const requests = await modelRequestsForPrompt( + "Reply with SDK_DISPLAY_AGENT_DONE exactly." + ); + expect(requests.length).toBeGreaterThan(0); + for (const request of requests) { + expect(request).toMatchObject({ + model: "claude-sonnet-5", + reasoning_effort: "high", + }); + } + const parentRequests = await modelRequestsForPrompt( + "After it completes, reply with SDK_DISPLAY_PARENT_DONE exactly." + ); + expect(parentRequests.length).toBeGreaterThan(0); + for (const request of parentRequests) { + expect(request.reasoning_effort).toBe("medium"); + } + } finally { + unsubscribe(); + } + } finally { + await recordingClient.stop(); + } + }, + 240_000 + ); + it("should return expected results for missing pending handler requestIds", async () => { const session = await client.createSession({ onPermissionRequest: approveAll }); diff --git a/nodejs/test/e2e/sandbox_devices.e2e.test.ts b/nodejs/test/e2e/sandbox_devices.e2e.test.ts new file mode 100644 index 0000000000..173289e71a --- /dev/null +++ b/nodejs/test/e2e/sandbox_devices.e2e.test.ts @@ -0,0 +1,86 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +import { readFile, writeFile } from "node:fs/promises"; +import { join } from "node:path"; +import { describe, it } from "vitest"; +import type { PermissionRequest } from "../../src/index.js"; +import { createSdkTestContext } from "./harness/sdkTestContext.js"; + +const TEST_NAME = "does not offer a sandbox bypass for standard device paths"; + +describe("Sandbox devices", async () => { + if (process.platform !== "darwin") { + // The SDK sandbox suite currently has a backend only on macOS. + it.skip(TEST_NAME, () => undefined); + return; + } + + const { copilotClient: client, workDir } = await createSdkTestContext({ + copilotClientOptions: { + env: { COPILOT_CLI_ENABLED_FEATURE_FLAGS: "SANDBOX" }, + }, + }); + + it(TEST_NAME, { timeout: 180_000 }, async ({ expect }) => { + const denialText = "cat: /dev/stdin: Operation not permitted\n"; + await writeFile(join(workDir, "denial.txt"), denialText); + const bypassRequests: PermissionRequest[] = []; + const shellResults: string[] = []; + const shellCalls = new Set(); + const session = await client.createSession({ + onPermissionRequest: (request) => { + if ("requestSandboxBypass" in request && request.requestSandboxBypass) { + bypassRequests.push(request); + return { kind: "reject" }; + } + return { kind: "approve-once" }; + }, + }); + try { + const update = await session.rpc.options.update({ + sandboxConfig: { + enabled: true, + allowBypass: true, + addCurrentWorkingDirectory: true, + }, + }); + expect(update.success).toBe(true); + session.on((event) => { + if (event.type === "tool.execution_start" && event.data.toolName === "bash") { + shellCalls.add(event.data.toolCallId); + } else if ( + event.type === "tool.execution_complete" && + shellCalls.has(event.data.toolCallId) && + event.data.success + ) { + shellResults.push(event.data.result?.content ?? ""); + } + }); + + const message = await session.sendAndWait( + { + prompt: + "Use the bash tool exactly once to run this exact command: " + + "`head -c 16 /dev/urandom > entropy.bin && " + + "cat /dev/stdin < denial.txt > copy.txt && cat copy.txt`. " + + "Do not request a sandbox bypass. After the tool returns, " + + "reply with exactly DEVICE_COMMAND_COMPLETE.", + }, + 120_000 + ); + + expect(message?.data.content).toContain("DEVICE_COMMAND_COMPLETE"); + expect(shellCalls.size).toBe(1); + expect(shellResults).toHaveLength(1); + expect(shellResults[0]).toContain(denialText.trim()); + expect(shellResults[0]).toContain("exit code 0"); + expect(await readFile(join(workDir, "entropy.bin"))).toHaveLength(16); + expect(await readFile(join(workDir, "copy.txt"), "utf8")).toBe(denialText); + expect(bypassRequests).toEqual([]); + } finally { + await session.disconnect(); + } + }); +}); diff --git a/nodejs/test/e2e/sandbox_filesystem.e2e.test.ts b/nodejs/test/e2e/sandbox_filesystem.e2e.test.ts new file mode 100644 index 0000000000..80a7baf04a --- /dev/null +++ b/nodejs/test/e2e/sandbox_filesystem.e2e.test.ts @@ -0,0 +1,62 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +import { existsSync } from "node:fs"; +import { readFile, stat } from "node:fs/promises"; +import { join } from "node:path"; +import { describe, it } from "vitest"; +import { approveAll } from "../../src/index.js"; +import { createSdkTestContext } from "./harness/sdkTestContext.js"; + +const TEST_NAME = "creates missing denied directories on the host before a sandboxed shell runs"; + +describe("Sandbox filesystem", async () => { + if (process.platform !== "darwin") { + // The SDK sandbox suite currently has a backend only on macOS. + it.skip(TEST_NAME, () => undefined); + return; + } + + const { copilotClient: client, workDir } = await createSdkTestContext({ + copilotClientOptions: { + env: { COPILOT_CLI_ENABLED_FEATURE_FLAGS: "SANDBOX" }, + }, + }); + + it(TEST_NAME, { timeout: 120_000 }, async ({ expect }) => { + const denied = join(workDir, "vault", ".env"); + expect(existsSync(denied)).toBe(false); + const session = await client.createSession({ onPermissionRequest: approveAll }); + try { + const update = await session.rpc.options.update({ + sandboxConfig: { + enabled: true, + allowBypass: false, + addCurrentWorkingDirectory: true, + userPolicy: { + filesystem: { deniedPaths: ["./vault/.env"] }, + network: { allowOutbound: false }, + }, + }, + }); + expect(update.success).toBe(true); + const result = await session.rpc.shell.executeUserRequested({ + requestId: "denied-directory", + command: + "echo control > control.txt && " + + "if echo blocked > vault/.env/probe; then echo SANDBOX_WROTE; " + + "else echo SANDBOX_DENIED; fi", + }); + expect(result.success, result.output).toBe(true); + expect(result.exitCode, result.output).toBe(0); + expect(result.output).toContain("SANDBOX_DENIED"); + expect(result.output).not.toContain("SANDBOX_WROTE"); + expect((await stat(denied)).isDirectory()).toBe(true); + expect(existsSync(join(denied, "probe"))).toBe(false); + expect((await readFile(join(workDir, "control.txt"), "utf8")).trim()).toBe("control"); + } finally { + await session.disconnect(); + } + }); +}); diff --git a/nodejs/test/e2e/sandbox_gh_config.e2e.test.ts b/nodejs/test/e2e/sandbox_gh_config.e2e.test.ts new file mode 100644 index 0000000000..69542c5d9f --- /dev/null +++ b/nodejs/test/e2e/sandbox_gh_config.e2e.test.ts @@ -0,0 +1,138 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +import { execFileSync } from "node:child_process"; +import { readFile, writeFile } from "node:fs/promises"; +import { join } from "node:path"; +import { describe, it } from "vitest"; +import { approveAll } from "../../src/index.js"; +import { createSdkTestContext, DEFAULT_GITHUB_TOKEN } from "./harness/sdkTestContext.js"; + +const TEST_NAME = + "preserves gh settings and aliases in sandboxed scripts without copying credentials"; + +describe("Sandbox gh configuration", async () => { + if (process.platform !== "darwin") { + // The SDK sandbox suite currently has a backend only on macOS. + it.skip(TEST_NAME, () => undefined); + return; + } + + const { + copilotClient: client, + homeDir, + workDir, + } = await createSdkTestContext({ + copilotClientOptions: { + gitHubToken: DEFAULT_GITHUB_TOKEN, + env: { + COPILOT_CLI_ENABLED_FEATURE_FLAGS: "SANDBOX", + GH_TOKEN: DEFAULT_GITHUB_TOKEN, + GITHUB_TOKEN: DEFAULT_GITHUB_TOKEN, + GITHUB_COPILOT_GITHUB_TOKEN: DEFAULT_GITHUB_TOKEN, + GIT_CONFIG_GLOBAL: "/dev/null", + GIT_CONFIG_NOSYSTEM: "1", + }, + }, + }); + + it(TEST_NAME, { timeout: 120_000 }, async ({ expect }) => { + execFileSync("git", ["init", "--quiet"], { cwd: workDir, stdio: "pipe" }); + const token = `ghp_${"a".repeat(36)}`; + const source = join(homeDir, "config.yml"); + const config = (editor: string) => + [ + 'version: "1"', + `editor: ${editor}`, + "pager:", + "browser:", + "http_unix_socket:", + "aliases:", + ` sandbox-check: ${JSON.stringify( + `!case "$GITHUB_TOKEN" in copilot_mask_*) printf '%s\\n' GH_ALIAS_AVAILABLE ;; *) exit 73 ;; esac` + )}`, + " token: auth token", + " who: auth token", + ` embedded-secret: ${JSON.stringify(`!echo ${token}`)}`, + "hosts:", + " github.com:", + ` oauth_token: ${token}`, + "", + ].join("\n"); + await writeFile(source, config("sandbox-editor-one")); + await writeFile(join(homeDir, "hosts.yml"), `github.com:\n oauth_token: ${token}\n`); + await writeFile( + join(workDir, "release.sh"), + [ + "set -eu", + 'case "$GH_TOKEN" in copilot_mask_*) ;; *) exit 71 ;; esac', + 'test ! -e "$GH_CONFIG_DIR/hosts.yml"', + `if cat ${JSON.stringify(join(homeDir, "hosts.yml"))} >/dev/null 2>&1; then exit 72; fi`, + 'cp "$GH_CONFIG_DIR/config.yml" copied-config.yml', + 'printf "%s" "$GH_CONFIG_DIR" > copied-config-dir.txt', + "gh sandbox-check", + 'test -z "$(gh config get http_unix_socket)"', + "gh alias list", + "gh config get editor", + "printf '%s\\n' GH_SCRIPT_COMPLETE", + "", + ].join("\n") + ); + + const session = await client.createSession({ onPermissionRequest: approveAll }); + try { + const update = await session.rpc.options.update({ + sandboxConfig: { + enabled: true, + addCurrentWorkingDirectory: true, + auth: { git: false, gh: true }, + userPolicy: { + filesystem: { deniedPaths: [homeDir] }, + network: { allowOutbound: false }, + }, + }, + }); + expect(update.success).toBe(true); + + let previousDirectory: string | undefined; + for (const editor of ["sandbox-editor-one", "sandbox-editor-two"]) { + const original = config(editor); + await writeFile(source, original); + const result = await session.rpc.shell.executeUserRequested({ + requestId: editor, + command: "sh release.sh", + }); + expect(result.success, result.output).toBe(true); + expect(result.exitCode, result.output).toBe(0); + expect(result.output).toContain("GH_ALIAS_AVAILABLE"); + expect(result.output).toContain("token: auth token"); + expect(result.output).toContain("who: auth token"); + expect(result.output).toContain(editor); + expect(result.output).toContain("GH_SCRIPT_COMPLETE"); + + // Read raw child-written bytes, not secret-filtered tool output. + const copied = await readFile(join(workDir, "copied-config.yml"), "utf8"); + expect(copied).not.toContain(token); + expect(copied).not.toContain("oauth_token"); + expect(copied).not.toContain("hosts:"); + expect(copied).toContain("sandbox-check"); + expect(await readFile(source, "utf8")).toBe(original); + const directory = await readFile(join(workDir, "copied-config-dir.txt"), "utf8"); + expect(directory).not.toBe(homeDir); + expect(directory).not.toBe(previousDirectory); + previousDirectory = directory; + + const direct = await session.rpc.shell.executeUserRequested({ + requestId: `${editor}-direct-alias`, + command: "gh sandbox-check", + }); + expect(direct.success, direct.output).toBe(true); + expect(direct.exitCode, direct.output).toBe(0); + expect(direct.output).toContain("GH_ALIAS_AVAILABLE"); + } + } finally { + await session.disconnect(); + } + }); +}); diff --git a/nodejs/test/e2e/session.e2e.test.ts b/nodejs/test/e2e/session.e2e.test.ts index d9f1124291..10f3b49f8a 100644 --- a/nodejs/test/e2e/session.e2e.test.ts +++ b/nodejs/test/e2e/session.e2e.test.ts @@ -1,5 +1,5 @@ import { rm } from "fs/promises"; -import { describe, expect, it, onTestFinished, vi } from "vitest"; +import { describe, expect, it, onTestFailed, onTestFinished, vi } from "vitest"; import { ParsedHttpExchange } from "../../../test/harness/replayingCapiProxy.js"; import { CopilotClient, approveAll, defineTool, RuntimeConnection } from "../../src/index.js"; import { createSdkTestContext, DEFAULT_GITHUB_TOKEN, isCI } from "./harness/sdkTestContext.js"; @@ -99,7 +99,7 @@ describe("Sessions", () => { it("should recover marker after cold resume with explicit session id", async () => { const sessionId = `e2e-resume-${Date.now()}`; const marker = "MARKER-7f3ac21e"; - const firstClient = new CopilotClient({ + const firstClient = createClient({ workingDirectory: workDir, env, connection: RuntimeConnection.forStdio({ path: process.env.COPILOT_CLI_PATH }), @@ -127,7 +127,7 @@ describe("Sessions", () => { await session.disconnect(); await firstClient.stop(); - const secondClient = new CopilotClient({ + const secondClient = createClient({ workingDirectory: workDir, env, connection: RuntimeConnection.forStdio({ path: process.env.COPILOT_CLI_PATH }), @@ -241,6 +241,9 @@ describe("Sessions", () => { content: systemMessageSuffix, }, }); + await session.rpc.tools.updateSubagentSettings({ + subagents: { agents: { "general-purpose": { model: "claude-haiku-4.5" } } }, + }); const assistantMessage = await session.sendAndWait({ prompt: "What is your full name?" }); expect(assistantMessage?.data.content).toContain("GitHub"); @@ -251,6 +254,8 @@ describe("Sessions", () => { const systemMessage = getSystemMessage(traffic[0]); expect(systemMessage).toContain("GitHub"); expect(systemMessage).toContain(systemMessageSuffix); + expect(systemMessage?.match(/\n/g)).toHaveLength(1); + expect(systemMessage).toContain("- general-purpose: claude-haiku-4.5"); }); it("should create a session with replaced systemMessage config", async () => { @@ -259,6 +264,9 @@ describe("Sessions", () => { onPermissionRequest: approveAll, systemMessage: { mode: "replace", content: testSystemMessage }, }); + await session.rpc.tools.updateSubagentSettings({ + subagents: { agents: { "general-purpose": { model: "claude-haiku-4.5" } } }, + }); const assistantMessage = await session.sendAndWait({ prompt: "What is your full name?" }); expect(assistantMessage?.data.content).not.toContain("GitHub"); @@ -283,12 +291,17 @@ describe("Sessions", () => { sections: { tone: { action: "replace", content: customTone }, code_change_rules: { action: "remove" }, + last_instructions: { action: "remove" }, }, content: appendedContent, }, }); + await session.rpc.tools.updateSubagentSettings({ + subagents: { agents: { "general-purpose": { model: "claude-haiku-4.5" } } }, + }); - await session.send({ prompt: "Who are you?" }); + const assistantMessage = await session.sendAndWait({ prompt: "Who are you?" }); + expect(assistantMessage?.type).toBe("assistant.message"); // Validate the system message sent to the model const traffic = await waitForExchanges(); @@ -297,6 +310,15 @@ describe("Sessions", () => { expect(systemMessage).toContain(appendedContent); // The code_change_rules section should have been removed expect(systemMessage).not.toContain(""); + expect(systemMessage).not.toContain("\n"); + expect(systemMessage).not.toContain("- general-purpose: claude-haiku-4.5"); + expect(traffic[0].request.tools).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + function: expect.objectContaining({ name: "task" }), + }), + ]) + ); } ); @@ -400,26 +422,41 @@ describe("Sessions", () => { }); it("should resume a session using the same client", async () => { + let phase = "create session"; + const events: string[] = []; + const onEvent = (event: { type: string }) => { + events.push(event.type); + }; + onTestFailed(() => { + console.error("Same-client resume diagnostics", { phase, events }); + }); + // Create initial session - const session1 = await client.createSession({ onPermissionRequest: approveAll }); + const session1 = await client.createSession({ onPermissionRequest: approveAll, onEvent }); const sessionId = session1.sessionId; + phase = "first sendAndWait"; const answer = await session1.sendAndWait({ prompt: "What is 1+1?" }); expect(answer?.data.content).toContain("2"); // Resume using the same client + phase = "resume session"; await using session2 = await client.resumeSession(sessionId, { onPermissionRequest: approveAll, + onEvent, }); expect(session2.sessionId).toBe(sessionId); + phase = "read resumed history"; const messages = await session2.getEvents(); const assistantMessages = messages.filter((m) => m.type === "assistant.message"); expect(assistantMessages[assistantMessages.length - 1].data.content).toContain("2"); // Can continue the conversation statefully + phase = "second sendAndWait"; const secondAssistantMessage = await session2.sendAndWait({ prompt: "Now if you double that, what do you get?", }); expect(secondAssistantMessage?.data.content).toContain("4"); + phase = "disconnect resumed session"; }); it("should resume a session using a new client", async () => { @@ -657,11 +694,13 @@ describe("Sessions", () => { }); it("should create session with custom config dir", async () => { + // Stop the config directory's writer before the fixture removes its files. + await using customClient = createClient(); const customConfigDir = `${homeDir}/custom-config`; onTestFinished(async () => { await rm(customConfigDir, { recursive: true, force: true }).catch(() => {}); }); - await using session = await client.createSession({ + await using session = await customClient.createSession({ onPermissionRequest: approveAll, configDirectory: customConfigDir, }); diff --git a/nodejs/test/e2e/session_config.e2e.test.ts b/nodejs/test/e2e/session_config.e2e.test.ts index de81e8be1d..0abd7a56f5 100644 --- a/nodejs/test/e2e/session_config.e2e.test.ts +++ b/nodejs/test/e2e/session_config.e2e.test.ts @@ -1,4 +1,6 @@ import { describe, expect, it } from "vitest"; +import { execFileSync } from "node:child_process"; +import { realpathSync } from "node:fs"; import { writeFile, mkdir } from "fs/promises"; import { join } from "path"; import { @@ -10,6 +12,9 @@ import { } from "../../src/index.js"; import { createSdkTestContext, DEFAULT_GITHUB_TOKEN } from "./harness/sdkTestContext.js"; import { retry } from "./harness/sdkTestHelper.js"; +import { isByokBackend } from "./harness/testBackend"; + +const capiIt = it.skipIf(isByokBackend); describe("Session Configuration", async () => { const { copilotClient: client, workDir, openAiEndpoint, env } = await createSdkTestContext(); @@ -44,7 +49,7 @@ describe("Session Configuration", async () => { await session.disconnect(); }); - it("should create session with custom provider config", async () => { + capiIt("should create session with custom provider config", async () => { const session = await client.createSession({ onPermissionRequest: approveAll, provider: { @@ -62,7 +67,8 @@ describe("Session Configuration", async () => { } }); - it("should accept blob attachments", async () => { + // Preserve the existing .NET BYOK exclusions: image replay diverges for provider-backed sessions. + capiIt("should accept blob attachments", async () => { // Write the image to disk so the model can view it if it tries const pngBase64 = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg=="; @@ -114,7 +120,7 @@ describe("Session Configuration", async () => { ); } - it("vision disabled then enabled via setModel", async () => { + capiIt("vision disabled then enabled via setModel", async () => { await writeFile(join(workDir, "test.png"), PNG_1X1); const session = await client.createSession({ @@ -145,7 +151,7 @@ describe("Session Configuration", async () => { await session.disconnect(); }); - it("vision enabled then disabled via setModel", async () => { + capiIt("vision enabled then disabled via setModel", async () => { await writeFile(join(workDir, "test.png"), PNG_1X1); const session = await client.createSession({ @@ -291,8 +297,26 @@ describe("Session Configuration", async () => { body: string; } + /** Visible Anthropic thinking, with the provider-reported count if any. */ + interface AnthropicReasoningFixture { + tokens?: number; + } + + const thinkingBlock = { + type: "thinking", + thinking: "Visible thinking is not measured usage.", + signature: "test-signature", + }; + + function reportedThinkingTokens(reasoning?: AnthropicReasoningFixture) { + return reasoning?.tokens === undefined + ? {} + : { output_tokens_details: { thinking_tokens: reasoning.tokens } }; + } + class RecordingRequestHandler extends CopilotRequestHandler { readonly records: InterceptedRequest[] = []; + reasoning?: AnthropicReasoningFixture; protected override async sendRequest( request: Request, @@ -301,7 +325,7 @@ describe("Session Configuration", async () => { const body = request.body ? await request.text() : ""; this.records.push({ url: request.url, body }); return isInferenceUrl(request.url) - ? buildInferenceResponse(request.url, body) + ? buildInferenceResponse(request.url, body, this.reasoning) : buildNonInferenceResponse(request.url); } @@ -334,7 +358,10 @@ describe("Session Configuration", async () => { }); } - function anthropicMessageStreamBody(text: string): string { + function anthropicMessageStreamBody( + text: string, + reasoning?: AnthropicReasoningFixture + ): string { const events: Array<[string, unknown]> = [ [ "message_start", @@ -356,25 +383,40 @@ describe("Session Configuration", async () => { "content_block_start", { type: "content_block_start", - index: 0, + index: reasoning ? 1 : 0, content_block: { type: "text", text: "" }, }, ], [ "content_block_delta", - { type: "content_block_delta", index: 0, delta: { type: "text_delta", text } }, + { + type: "content_block_delta", + index: reasoning ? 1 : 0, + delta: { type: "text_delta", text }, + }, ], - ["content_block_stop", { type: "content_block_stop", index: 0 }], + ["content_block_stop", { type: "content_block_stop", index: reasoning ? 1 : 0 }], [ "message_delta", { type: "message_delta", delta: { stop_reason: "end_turn", stop_sequence: null }, - usage: { output_tokens: 7 }, + usage: { output_tokens: 7, ...reportedThinkingTokens(reasoning) }, }, ], ["message_stop", { type: "message_stop" }], ]; + if (reasoning) { + events.splice( + 1, + 0, + [ + "content_block_start", + { type: "content_block_start", index: 0, content_block: thinkingBlock }, + ], + ["content_block_stop", { type: "content_block_stop", index: 0 }] + ); + } return events .map(([event, data]) => `event: ${event}\ndata: ${JSON.stringify(data)}\n\n`) .join(""); @@ -414,22 +456,29 @@ describe("Session Configuration", async () => { return json({}); } - function buildInferenceResponse(url: string, body: string): Response { + function buildInferenceResponse( + url: string, + body: string, + reasoning?: AnthropicReasoningFixture + ): Response { const u = url.toLowerCase(); const wantsStream = /"stream"\s*:\s*true/.test(body); if (u.endsWith("/messages")) { if (wantsStream) { - return sse(anthropicMessageStreamBody("OK from the synthetic stream.")); + return sse(anthropicMessageStreamBody("OK from the synthetic stream.", reasoning)); } return json({ id: "msg_stub_1", type: "message", role: "assistant", model: "claude-sonnet-5", - content: [{ type: "text", text: "OK from the synthetic stream." }], + content: [ + ...(reasoning ? [thinkingBlock] : []), + { type: "text", text: "OK from the synthetic stream." }, + ], stop_reason: "end_turn", stop_sequence: null, - usage: { input_tokens: 5, output_tokens: 7 }, + usage: { input_tokens: 5, output_tokens: 7, ...reportedThinkingTokens(reasoning) }, }); } return json({ @@ -551,7 +600,7 @@ describe("Session Configuration", async () => { await session1.disconnect(); }); - it("should enable citations for Anthropic file attachments on create", async () => { + capiIt("should enable citations for Anthropic file attachments on create", async () => { const handler = new RecordingRequestHandler(); const citationClient = new CopilotClient({ connection: RuntimeConnection.forStdio({ path: process.env.COPILOT_CLI_PATH }), @@ -584,7 +633,52 @@ describe("Session Configuration", async () => { } }); - it("should enable citations for Anthropic file attachments on resume", async () => { + it.each( + [false, true].flatMap((streaming) => + [undefined, 0, 7].map((tokens) => ({ streaming, tokens })) + ) + )( + "reports Anthropic reasoning tokens only when the provider does (streaming: $streaming, tokens: $tokens)", + async ({ streaming, tokens }) => { + const handler = new RecordingRequestHandler(); + handler.reasoning = { tokens }; + const usageClient = new CopilotClient({ + connection: RuntimeConnection.forStdio({ path: process.env.COPILOT_CLI_PATH }), + workingDirectory: workDir, + env, + gitHubToken: DEFAULT_GITHUB_TOKEN, + requestHandler: handler, + }); + await usageClient.start(); + try { + const session = await usageClient.createSession({ + onPermissionRequest: approveAll, + model: "claude-sonnet-5", + provider: createAnthropicProvider(), + streaming, + }); + try { + const usages: Array> = []; + session.on("assistant.usage", (event) => usages.push(event.data)); + const response = await session.sendAndWait({ prompt: "Reply briefly." }); + expect(response?.data.content).toBe("OK from the synthetic stream."); + expect(usages).toHaveLength(1); + expect(usages[0]).toMatchObject({ inputTokens: 5, outputTokens: 7 }); + if (tokens === undefined) { + expect(usages[0]).not.toHaveProperty("reasoningTokens"); + } else { + expect(usages[0].reasoningTokens).toBe(tokens); + } + } finally { + await session.disconnect(); + } + } finally { + await usageClient.stop(); + } + } + ); + + capiIt("should enable citations for Anthropic file attachments on resume", async () => { const handler = new RecordingRequestHandler(); const connectionToken = "ts-citation-resume-token"; const serverClient = new CopilotClient({ @@ -600,7 +694,9 @@ describe("Session Configuration", async () => { await serverClient.start(); try { - const session1 = await serverClient.createSession({ onPermissionRequest: approveAll }); + const session1 = await serverClient.createSession({ + onPermissionRequest: approveAll, + }); const port = (serverClient as unknown as { runtimePort: number | null }).runtimePort; expect(port).not.toBeNull(); const resumeClient = new CopilotClient({ @@ -693,7 +789,7 @@ describe("Session Configuration", async () => { await session1.disconnect(); }); - it("should refresh cached custom instructions only when creating with opt-in", async () => { + capiIt("should refresh cached custom instructions only when creating with opt-in", async () => { const projectDir = join(workDir, "instruction-refresh-project"); const instructionDir = join(workDir, "refresh-instructions"); const instructionFilesDir = join(instructionDir, ".github", "instructions"); @@ -755,7 +851,100 @@ describe("Session Configuration", async () => { } }); - it("should forward clientName in user-agent", async () => { + it("should include instructions discovered after repository creation in the next turn", async () => { + const projectDir = join(workDir, "deferred-repository-instructions"); + const marker = "SDK_RELOAD_DEFERRED_REPOSITORY_INSTRUCTIONS"; + await mkdir(projectDir); + + const handler = new RecordingRequestHandler(); + const refreshClient = new CopilotClient({ + connection: RuntimeConnection.forStdio({ path: process.env.COPILOT_CLI_PATH }), + workingDirectory: projectDir, + env, + gitHubToken: DEFAULT_GITHUB_TOKEN, + requestHandler: handler, + }); + const session = await refreshClient.createSession({ + onPermissionRequest: approveAll, + workingDirectory: projectDir, + model: "claude-sonnet-5", + provider: createAnthropicProvider(), + }); + + try { + const before = await session.rpc.metadata.snapshot(); + expect((await session.sendAndWait({ prompt: "Say OK." }))?.data.content).toBe( + "OK from the synthetic stream." + ); + await retry("observe the initial session context", async () => { + const metadata = await refreshClient.getSessionMetadata(session.sessionId); + expect(metadata?.context?.workingDirectory).toBe(projectDir); + }); + const initialContext = (await refreshClient.getSessionMetadata(session.sessionId)) + ?.context; + expect(initialContext?.gitRoot).toBeUndefined(); + expect(handler.inferenceRequests()).toHaveLength(1); + const firstRequest = JSON.parse(handler.inferenceRequests()[0].body) as { + system: unknown; + }; + expect(firstRequest.system).toBeDefined(); + expect(JSON.stringify(firstRequest.system)).not.toContain(marker); + + execFileSync("git", ["init", "--quiet"], { cwd: projectDir }); + const instructionsDir = join(projectDir, ".github"); + await mkdir(instructionsDir); + await writeFile( + join(instructionsDir, "copilot-instructions.md"), + `Repository instructions: ${marker}\n` + ); + + const reload = await session.rpc.customizations.reload(); + expect(reload.errors).toEqual([]); + expect(reload.outcomes).toEqual( + expect.arrayContaining([ + expect.objectContaining({ subsystem: "repositoryContext", status: "reloaded" }), + expect.objectContaining({ subsystem: "instructions", status: "reloaded" }), + ]) + ); + await retry("observe the refreshed repository context", async () => { + const gitRoot = (await refreshClient.getSessionMetadata(session.sessionId))?.context + ?.gitRoot; + expect(gitRoot && realpathSync.native(gitRoot)).toBe( + realpathSync.native(projectDir) + ); + }); + expect((await session.rpc.metadata.snapshot()).workingDirectory).toBe( + before.workingDirectory + ); + expect( + (await session.rpc.instructions.getSources()).sources.some( + (source) => + source.type === "repo" && + source.location === "repository" && + source.content.includes(marker) + ) + ).toBe(true); + + expect((await session.sendAndWait({ prompt: "Say OK again." }))?.data.content).toBe( + "OK from the synthetic stream." + ); + expect(handler.inferenceRequests()).toHaveLength(2); + const nextRequest = JSON.parse(handler.inferenceRequests()[1].body) as { + system: unknown; + }; + expect(nextRequest.system).toBeDefined(); + expect(JSON.stringify(nextRequest.system)).toContain(marker); + } finally { + try { + await session.disconnect(); + } finally { + expect(await refreshClient.stop()).toEqual([]); + } + } + }); + + // This asserts CAPI's user-agent; BYOK providers supply their own. + capiIt("should forward clientName in user-agent", async () => { const session = await client.createSession({ onPermissionRequest: approveAll, clientName: CLIENT_NAME, @@ -772,7 +961,7 @@ describe("Session Configuration", async () => { await session.disconnect(); }); - it("should forward custom provider headers on create", async () => { + capiIt("should forward custom provider headers on create", async () => { const session = await client.createSession({ onPermissionRequest: approveAll, model: "claude-sonnet-5", @@ -792,7 +981,7 @@ describe("Session Configuration", async () => { await session.disconnect(); }); - it("should forward custom provider headers on resume", async () => { + capiIt("should forward custom provider headers on resume", async () => { const session1 = await client.createSession({ onPermissionRequest: approveAll }); const sessionId = session1.sessionId; @@ -816,7 +1005,7 @@ describe("Session Configuration", async () => { await session2.disconnect(); }); - it("should forward provider wire model", async () => { + capiIt("should forward provider wire model", async () => { // Verifies that ProviderConfig.wireModel overrides the model name sent to // the provider API, while SessionConfig.model still drives runtime // configuration lookup (capabilities, prompts, reasoning behavior). @@ -845,7 +1034,7 @@ describe("Session Configuration", async () => { await session.disconnect(); }); - it("should use provider model id as wire model", async () => { + capiIt("should use provider model id as wire model", async () => { // ProviderConfig.modelId drives both the runtime resolved model AND the wire // model when wireModel is not specified. SessionConfig.model is intentionally // omitted so that modelId is the only model source. @@ -930,10 +1119,13 @@ describe("Session Configuration", async () => { } }); - it("should apply GitHub MCP tool config on create", async () => { + // Built-in GitHub MCP requires Copilot authentication, not BYOK API-key authentication. + capiIt("should apply GitHub MCP tool config on create", async () => { const session = await client.createSession({ onPermissionRequest: approveAll, enableConfigDiscovery: true, + // Discover MCP configuration without starting unrelated standalone extensions. + requestExtensions: false, enableMcpApps: true, githubMcpToolConfig: { enableAllTools: true, diff --git a/nodejs/test/e2e/session_fs.e2e.test.ts b/nodejs/test/e2e/session_fs.e2e.test.ts index 9727de06e1..a987127965 100644 --- a/nodejs/test/e2e/session_fs.e2e.test.ts +++ b/nodejs/test/e2e/session_fs.e2e.test.ts @@ -4,7 +4,8 @@ import { SessionCompactionCompleteEvent } from "@github/copilot/sdk"; import { MemoryProvider, VirtualProvider } from "@platformatic/vfs"; -import { mkdtempSync, realpathSync } from "fs"; +import { createHash } from "crypto"; +import { existsSync, mkdtempSync, realpathSync } from "fs"; import { tmpdir } from "os"; import { join } from "path"; import { describe, expect, it, onTestFinished } from "vitest"; @@ -41,7 +42,12 @@ describe("Session Fs", async () => { const p = (sessionId: string, path: string) => `/${sessionId}${path.startsWith("/") ? path : "/" + path}`; - const { copilotClient: client, env } = await createSdkTestContext({ + const { + copilotClient: client, + createClient, + env, + openAiEndpoint, + } = await createSdkTestContext({ copilotClientOptions: { sessionFs: sessionFsConfig }, }); @@ -76,6 +82,74 @@ describe("Session Fs", async () => { } ); + it("should view an image that exists only in the binary session fs provider", async () => { + const imagePath = "/sdk-provider-image.png"; + const imageBytes = Buffer.from( + "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8DwHwAFBQIAX8jx0gAAAABJRU5ErkJggg==", + "base64" + ); + const binaryClient = createClient({ + sessionFs: { ...sessionFsConfig, capabilities: { binary: true } }, + }); + try { + let requestedPath: string | undefined; + const session = await binaryClient.createSession({ + onPermissionRequest: approveAll, + modelCapabilities: { supports: { vision: true } }, + createSessionFsProvider: (session) => ({ + ...createSessionFsProvider(session), + async readFileBytes(path) { + requestedPath = path; + return provider.readFile(p(session.sessionId, path)); + }, + async writeFileBytes(path, content) { + await provider.writeFile(p(session.sessionId, path), content); + }, + }), + }); + await provider.mkdir(`/${session.sessionId}`, { recursive: true }); + await provider.writeFile(p(session.sessionId, imagePath), imageBytes); + expect(existsSync(imagePath)).toBe(false); + + const msg = await session.sendAndWait({ + prompt: "Use the view tool to view /sdk-provider-image.png, then reply with exactly SDK_PROVIDER_IMAGE_DONE.", + }); + expect(msg?.data.content).toContain("SDK_PROVIDER_IMAGE_DONE"); + expect(requestedPath).toBe(imagePath); + const events = await session.getEvents(); + const assetId = `sha256:${createHash("sha256").update(imageBytes).digest("hex")}`; + expect( + events.some( + (event) => + event.type === "session.binary_asset" && + event.data.assetId === assetId && + event.data.mimeType === "image/png" && + event.data.data === imageBytes.toString("base64") + ) + ).toBe(true); + expect( + events.some( + (event) => + event.type === "tool.execution_complete" && + event.data.success && + event.data.result?.binaryResultsForLlm?.some( + (result) => "assetId" in result && result.assetId === assetId + ) + ) + ).toBe(true); + expect( + (await openAiEndpoint.getRequests()) + .filter((request) => + ["/chat/completions", "/responses", "/v1/messages"].includes(request.url) + ) + .some((request) => request.body.includes(imageBytes.toString("base64"))) + ).toBe(true); + await session.disconnect(); + } finally { + await binaryClient.stop(); + } + }); + it("should load session data from fs provider on resume", async () => { const session1 = await client.createSession({ onPermissionRequest: approveAll, diff --git a/nodejs/test/e2e/streaming_fidelity.e2e.test.ts b/nodejs/test/e2e/streaming_fidelity.e2e.test.ts index 98b8eb1884..89e1cb1dc1 100644 --- a/nodejs/test/e2e/streaming_fidelity.e2e.test.ts +++ b/nodejs/test/e2e/streaming_fidelity.e2e.test.ts @@ -5,9 +5,10 @@ import { describe, expect, it, onTestFinished } from "vitest"; import { SessionEvent, approveAll } from "../../src/index.js"; import { createSdkTestContext, isCI } from "./harness/sdkTestContext"; +import { isByokBackend, testBackend } from "./harness/testBackend"; describe("Streaming Fidelity", async () => { - const { copilotClient: client, createClient } = await createSdkTestContext(); + const { copilotClient: client, createClient, openAiEndpoint } = await createSdkTestContext(); it("should produce delta events when streaming is enabled", async () => { const session = await client.createSession({ @@ -77,7 +78,8 @@ describe("Streaming Fidelity", async () => { onPermissionRequest: approveAll, streaming: false, }); - await session.sendAndWait({ prompt: "What is 3 + 6?" }); + const firstAssistantMessage = await session.sendAndWait({ prompt: "What is 3 + 6?" }); + expect(firstAssistantMessage?.data.content).toContain("9"); await session.disconnect(); // Resume using a new client @@ -97,6 +99,26 @@ describe("Streaming Fidelity", async () => { }); expect(secondAssistantMessage?.data.content).toContain("18"); + const modelRequests = (await openAiEndpoint.getRequests()).filter((request) => + ["/chat/completions", "/v1/messages", "/responses"].includes(request.url) + ); + const endpoint = { + capi: "/chat/completions", + "anthropic-messages": "/v1/messages", + "openai-responses": "/responses", + "openai-completions": "/chat/completions", + }[testBackend]; + expect(modelRequests.length).toBeGreaterThanOrEqual(2); + for (const request of modelRequests) { + expect(request.url).toBe(endpoint); + if (isByokBackend) { + // Completions and CAPI share a URL; the BYOK credential distinguishes them. + expect(request.headers.authorization).toBe( + "Bearer fake-byok-credential-for-e2e-tests" + ); + } + } + // Should have streaming deltas before the final message const deltaEvents = events.filter((e) => e.type === "assistant.message_delta"); expect(deltaEvents.length).toBeGreaterThanOrEqual(1); diff --git a/nodejs/test/e2e/structured_output.e2e.test.ts b/nodejs/test/e2e/structured_output.e2e.test.ts index 0eb5957a61..e92125507f 100644 --- a/nodejs/test/e2e/structured_output.e2e.test.ts +++ b/nodejs/test/e2e/structured_output.e2e.test.ts @@ -14,6 +14,7 @@ import { } from "../../src/index.js"; import { createSdkTestContext, DEFAULT_GITHUB_TOKEN, isCI } from "./harness/sdkTestContext"; import { waitForCondition } from "./harness/sdkTestHelper"; +import { isByokBackend } from "./harness/testBackend"; describe("Structured output", async () => { const { copilotClient: client, openAiEndpoint } = await createSdkTestContext({ @@ -21,25 +22,30 @@ describe("Structured output", async () => { env: { COPILOT_CLI_ENABLED_FEATURE_FLAGS: "HYDRAFUSION,HYDRAFUSION_ROLLOUT" }, }, }); - const provider: ProviderConfig = { - type: "openai", - wireApi: "completions", - baseUrl: openAiEndpoint.url, - modelId: "gpt-4.1", - wireModel: "gpt-4.1", - apiKey: isCI ? DEFAULT_GITHUB_TOKEN : (process.env.GITHUB_TOKEN ?? DEFAULT_GITHUB_TOKEN), - headers: { - "Copilot-Integration-Id": "copilot-developer-cli", - "Copilot-Harness-Id": "copilot-sdk", - "X-GitHub-Api-Version": "2026-08-01", - }, - }; + const model = isByokBackend ? undefined : "gpt-4.1"; + const provider: ProviderConfig | undefined = isByokBackend + ? undefined + : { + type: "openai", + wireApi: "completions", + baseUrl: openAiEndpoint.url, + modelId: "gpt-4.1", + wireModel: "gpt-4.1", + apiKey: isCI + ? DEFAULT_GITHUB_TOKEN + : (process.env.GITHUB_TOKEN ?? DEFAULT_GITHUB_TOKEN), + headers: { + "Copilot-Integration-Id": "copilot-developer-cli", + "Copilot-Harness-Id": "copilot-sdk", + "X-GitHub-Api-Version": "2026-08-01", + }, + }; it("infers_typed_result_after_custom_tool", async () => { let calls = 0; const schema = z.object({ count: z.number().int(), color: z.string() }); const session = await client.createSession({ - model: "gpt-4.1", + model, provider, onPermissionRequest: approveAll, availableTools: [], @@ -71,7 +77,7 @@ describe("Structured output", async () => { expect(ordinary?.data.content.trim()).toBe("HELLO"); const exchanges = await openAiEndpoint.getExchanges(); expect(exchanges.length).toBeGreaterThanOrEqual(3); - for (const exchange of exchanges.slice(0, -1)) { + for (const exchange of isByokBackend ? [] : exchanges.slice(0, -1)) { expect(exchange.request).toHaveProperty( "response_format.json_schema.schema", schema.toJSONSchema() @@ -85,7 +91,7 @@ describe("Structured output", async () => { let calls = 0; let session: CopilotSession; session = await client.createSession({ - model: "gpt-4.1", + model, provider, onPermissionRequest: approveAll, availableTools: [], @@ -128,7 +134,7 @@ describe("Structured output", async () => { for (const exchange of exchanges.slice(1)) { expect(exchange.request).toHaveProperty("tool_choice", "none"); } - for (const exchange of exchanges) { + for (const exchange of isByokBackend ? [] : exchanges) { expect(exchange.request).toHaveProperty( "response_format.json_schema.schema", schema.toJSONSchema() @@ -141,7 +147,7 @@ describe("Structured output", async () => { let stops = 0; const replies: AssistantMessageEvent[] = []; const session = await client.createSession({ - model: "gpt-4.1", + model, provider, onPermissionRequest: approveAll, availableTools: [], @@ -188,7 +194,7 @@ describe("Structured output", async () => { const exchanges = await openAiEndpoint.getExchanges(); expect(exchanges).toHaveLength(3); expect(exchanges[1].request).toHaveProperty("tool_choice", "none"); - for (const exchange of exchanges) { + for (const exchange of isByokBackend ? [] : exchanges) { expect(exchange.request).toHaveProperty( "response_format.json_schema.schema", schema.toJSONSchema() @@ -247,7 +253,7 @@ describe("Structured output", async () => { releaseHook = resolve; }); const session = await client.createSession({ - model: "gpt-4.1", + model, provider, onPermissionRequest: approveAll, availableTools: [], @@ -319,7 +325,7 @@ describe("Structured output", async () => { const replies: AssistantMessageEvent[] = []; const schema = z.object({ answer: z.number().int() }); const session = await client.createSession({ - model: "gpt-4.1", + model, provider, onPermissionRequest: approveAll, availableTools: [], @@ -347,7 +353,7 @@ describe("Structured output", async () => { expect(replies[1].data.originatingMessageId).toBe(replies[0].data.originatingMessageId); const exchanges = await openAiEndpoint.getExchanges(); expect(exchanges).toHaveLength(2); - for (const exchange of exchanges) { + for (const exchange of isByokBackend ? [] : exchanges) { expect(exchange.request).toHaveProperty( "response_format.json_schema.schema", schema.toJSONSchema() @@ -362,7 +368,7 @@ describe("Structured output", async () => { const replies: AssistantMessageEvent[] = []; const schema = z.object({ answer: z.number().int() }); session = await client.createSession({ - model: "gpt-4.1", + model, provider, onPermissionRequest: approveAll, availableTools: [], @@ -394,7 +400,7 @@ describe("Structured output", async () => { expect(replies[1].data.originatingMessageId).toBe(replies[0].data.originatingMessageId); const exchanges = await openAiEndpoint.getExchanges(); expect(exchanges).toHaveLength(2); - for (const exchange of exchanges) { + for (const exchange of isByokBackend ? [] : exchanges) { expect(exchange.request).toHaveProperty( "response_format.json_schema.schema", schema.toJSONSchema() @@ -412,7 +418,7 @@ describe("Structured output", async () => { releaseTool = resolve; }); const session = await client.createSession({ - model: "gpt-4.1", + model, provider, onPermissionRequest: approveAll, availableTools: [], @@ -469,7 +475,7 @@ describe("Structured output", async () => { it("sends_explicit_schema_for_message_and_batch", async () => { const session = await client.createSession({ - model: "gpt-4.1", + model, provider, onPermissionRequest: approveAll, availableTools: [], diff --git a/nodejs/test/e2e/subagent_hooks.e2e.test.ts b/nodejs/test/e2e/subagent_hooks.e2e.test.ts index 767bc4b656..a892d62c84 100644 --- a/nodejs/test/e2e/subagent_hooks.e2e.test.ts +++ b/nodejs/test/e2e/subagent_hooks.e2e.test.ts @@ -12,10 +12,18 @@ import type { PreToolUseHookOutput, PostToolUseHookInput, PostToolUseHookOutput, + SubagentStartHookInput, + SubagentStopHookInput, } from "../../src/index.js"; import { approveAll, CopilotRequestHandler } from "../../src/index.js"; +import { responsesApiRequestToChatCompletion } from "../../../test/harness/responsesApiAdapter.js"; import { createSdkTestContext, isCI } from "./harness/sdkTestContext.js"; +const CHILD_CONTEXT = "Subagent start hook verified: read the requested file."; +const CHILD_CONTEXT_PREFIX = `${CHILD_CONTEXT}\n\n`; +const STOP_RESPONSE_PREFIX = "Subagent stop hook verified: "; +const SUBAGENT_TEST_TIMEOUT_MS = 120_000; + interface RequestRecord { url: string; agentId?: string; @@ -25,6 +33,8 @@ interface RequestRecord { class RecordingRequestHandler extends CopilotRequestHandler { readonly records: RequestRecord[] = []; + readonly childPromptsWithHookContext: string[] = []; + readonly parentRequestsWithModifiedResponse: string[] = []; protected override async sendRequest( request: Request, @@ -36,6 +46,48 @@ class RecordingRequestHandler extends CopilotRequestHandler { parentAgentId: ctx.parentAgentId, interactionType: ctx.interactionType, }); + if (isInferenceUrl(request.url)) { + const rawBody = await request.clone().text(); + const body = JSON.parse( + request.url.endsWith("/responses") + ? responsesApiRequestToChatCompletion(rawBody) + : rawBody + ) as { + messages?: { role: string; content?: unknown }[]; + input?: { role?: string; content?: unknown }[]; + }; + if (ctx.parentAgentId) { + // Responses uses input/input_text instead of messages/text. + for (const message of body.messages ?? body.input ?? []) { + if ( + message.role === "user" && + (typeof message.content === "string" || Array.isArray(message.content)) + ) { + const parts = + typeof message.content === "string" + ? [message.content] + : message.content + .filter( + ( + part + ): part is { + type: "text" | "input_text"; + text: string; + } => + (part?.type === "text" || + part?.type === "input_text") && + typeof part.text === "string" + ) + .map((part) => part.text); + this.childPromptsWithHookContext.push( + ...parts.filter((part) => part.includes(CHILD_CONTEXT_PREFIX)) + ); + } + } + } else if (JSON.stringify(body).includes(STOP_RESPONSE_PREFIX)) { + this.parentRequestsWithModifiedResponse.push(JSON.stringify(body)); + } + } return super.sendRequest(request, ctx); } } @@ -84,94 +136,154 @@ describe("Subagent hooks", async () => { }, }); - it("should invoke preToolUse and postToolUse hooks for sub-agent tool calls", async () => { - const hookLog: { kind: "pre" | "post"; toolName: string; sessionId: string }[] = []; - const waitingText = - "I've launched an explore agent to read subagent-test.txt. Waiting for it to complete..."; - const finalText = - "The explore agent successfully read the file. The contents of **subagent-test.txt** are:\n\n```\nHello from subagent test!\n```"; - const parentSessionId = randomUUID(); - let releaseView!: () => void; - const parentWaiting = new Promise((resolve) => { - releaseView = resolve; - }); + it( + "should apply subagent lifecycle hook outputs", + async () => { + const hookLog: { kind: "pre" | "post"; toolName: string; sessionId: string }[] = []; + const startInputs: { input: SubagentStartHookInput; invocationSessionId: string }[] = + []; + const stopInputs: { input: SubagentStopHookInput; invocationSessionId: string }[] = []; + const waitingText = + "I've launched an explore agent to read subagent-test.txt. Waiting for it to complete..."; + const finalText = + "The explore agent successfully read the file. The contents of **subagent-test.txt** are:\n\n```\nHello from subagent test!\n```"; + const parentSessionId = randomUUID(); + let releaseView!: () => void; + const parentWaiting = new Promise((resolve) => { + releaseView = resolve; + }); - const session = await client.createSession({ - sessionId: parentSessionId, - onPermissionRequest: approveAll, - hooks: { - onPreToolUse: async (input: PreToolUseHookInput) => { - hookLog.push({ - kind: "pre", - toolName: input.toolName, - sessionId: input.sessionId, - }); - return { permissionDecision: "allow" } as PreToolUseHookOutput; - }, - onPostToolUse: async (input: PostToolUseHookInput) => { - hookLog.push({ - kind: "post", - toolName: input.toolName, - sessionId: input.sessionId, - }); - // A fast child can inject its result before the parent ever asks for the fixture's waiting reply. - if (input.toolName === "view" && input.sessionId !== parentSessionId) - await parentWaiting; - return null as PostToolUseHookOutput; + const session = await client.createSession({ + sessionId: parentSessionId, + onPermissionRequest: approveAll, + hooks: { + onPreToolUse: async (input: PreToolUseHookInput) => { + hookLog.push({ + kind: "pre", + toolName: input.toolName, + sessionId: input.sessionId, + }); + return { permissionDecision: "allow" } as PreToolUseHookOutput; + }, + onPostToolUse: async (input: PostToolUseHookInput) => { + hookLog.push({ + kind: "post", + toolName: input.toolName, + sessionId: input.sessionId, + }); + // A fast child can inject its result before the parent ever asks for the fixture's waiting reply. + if (input.toolName === "view" && input.sessionId !== parentSessionId) + await parentWaiting; + return null as PostToolUseHookOutput; + }, + onSubagentStart: (input, invocation) => { + startInputs.push({ input, invocationSessionId: invocation.sessionId }); + return { additionalContext: CHILD_CONTEXT }; + }, + onSubagentStop: (input, invocation) => { + stopInputs.push({ input, invocationSessionId: invocation.sessionId }); + return { modifiedResponse: `${STOP_RESPONSE_PREFIX}${input.response}` }; + }, }, - }, - }); - - // Create a file for the sub-agent to read - await writeFile(join(workDir, "subagent-test.txt"), "Hello from subagent test!"); + }); + // Create a file for the sub-agent to read + await writeFile(join(workDir, "subagent-test.txt"), "Hello from subagent test!"); - const unsubscribe = session.on((event) => { - if ( - !event.agentId && - event.type === "assistant.message" && - event.data.content === waitingText - ) { + const unsubscribe = session.on((event) => { + if ( + !event.agentId && + event.type === "assistant.message" && + event.data.content === waitingText + ) { + releaseView(); + } + }); + try { + const response = await session.sendAndWait({ + prompt: "Use the task tool to spawn an explore agent that reads the file subagent-test.txt in the current directory and reports its contents. You must use the task tool.", + }); + expect(response?.agentId ?? "").toBe(""); + expect(response?.data.content).toBe(finalText); + expect( + (await session.getEvents()) + .filter((event) => !event.agentId && event.type === "assistant.message") + .map((event) => event.data.content) + .filter((content) => content === waitingText || content === finalText) + ).toEqual([waitingText, finalText]); + } finally { releaseView(); + unsubscribe(); } - }); - try { - const response = await session.sendAndWait({ - prompt: "Use the task tool to spawn an explore agent that reads the file subagent-test.txt in the current directory and reports its contents. You must use the task tool.", - }); - expect(response?.agentId ?? "").toBe(""); - expect(response?.data.content).toBe(finalText); + + // Parent tool hooks fire for "task" + const taskPre = hookLog.find((h) => h.kind === "pre" && h.toolName === "task"); expect( - (await session.getEvents()) - .filter((event) => !event.agentId && event.type === "assistant.message") - .map((event) => event.data.content) - .filter((content) => content === waitingText || content === finalText) - ).toEqual([waitingText, finalText]); - } finally { - releaseView(); - unsubscribe(); - } + taskPre, + "preToolUse should fire for the parent's 'task' tool call" + ).toBeDefined(); + + // Sub-agent tool hooks fire for "view" + const viewPre = hookLog.filter((h) => h.kind === "pre" && h.toolName === "view"); + const viewPost = hookLog.filter((h) => h.kind === "post" && h.toolName === "view"); + expect( + viewPre.length, + "preToolUse should fire for the sub-agent's 'view' tool call" + ).toBeGreaterThan(0); + expect( + viewPost.length, + "postToolUse should fire for the sub-agent's 'view' tool call" + ).toBeGreaterThan(0); + + // input.sessionId distinguishes parent from sub-agent: parent tools and + // sub-agent tools carry different sessionIds + expect(viewPre[0].sessionId).not.toBe(taskPre!.sessionId); + expectSubagentRequestMetadata(requestHandler.records); - // Parent tool hooks fire for "task" - const taskPre = hookLog.find((h) => h.kind === "pre" && h.toolName === "task"); - expect(taskPre, "preToolUse should fire for the parent's 'task' tool call").toBeDefined(); - - // Sub-agent tool hooks fire for "view" - const viewPre = hookLog.filter((h) => h.kind === "pre" && h.toolName === "view"); - const viewPost = hookLog.filter((h) => h.kind === "post" && h.toolName === "view"); - expect( - viewPre.length, - "preToolUse should fire for the sub-agent's 'view' tool call" - ).toBeGreaterThan(0); - expect( - viewPost.length, - "postToolUse should fire for the sub-agent's 'view' tool call" - ).toBeGreaterThan(0); - - // input.sessionId distinguishes parent from sub-agent: parent tools and - // sub-agent tools carry different sessionIds - expect(viewPre[0].sessionId).not.toBe(taskPre!.sessionId); - expectSubagentRequestMetadata(requestHandler.records); - - await session.disconnect(); - }, 120_000); + await expect + .poll(() => stopInputs.length, { timeout: SUBAGENT_TEST_TIMEOUT_MS }) + .toBeGreaterThan(0); + + const start = startInputs.find(({ input }) => input.agentName === "explore"); + const stop = stopInputs.find(({ input }) => input.agentType === "explore"); + expect(start, "subagentStart should fire for the explore agent").toBeDefined(); + expect(stop, "subagentStop should fire for the explore agent").toBeDefined(); + expect(start!.invocationSessionId).toBe(session.sessionId); + expect(start!.input.sessionId).toBe(session.sessionId); + expect(start!.input.timestamp).toBeInstanceOf(Date); + expect(start!.input.workingDirectory).toBe(workDir); + expect(typeof start!.input.transcriptPath).toBe("string"); + expect(start!.input.agentDisplayName).toBeUndefined(); + expect(start!.input.agentDescription).toBeUndefined(); + + expect(stop!.invocationSessionId).toBe(session.sessionId); + expect(stop!.input.sessionId).toBe(session.sessionId); + expect(stop!.input.timestamp).toBeInstanceOf(Date); + expect(stop!.input.timestamp.getTime()).toBeGreaterThanOrEqual( + start!.input.timestamp.getTime() + ); + expect(stop!.input.workingDirectory).toBe(workDir); + expect(stop!.input.transcriptPath).toBe(start!.input.transcriptPath); + expect(stop!.input.agentName).toBe(start!.input.agentName); + expect(stop!.input.agentDisplayName).toBe(start!.input.agentDisplayName); + expect(stop!.input.agentDescription).toBe(start!.input.agentDescription); + expect(stop!.input.agentId).toBeTruthy(); + expect(stop!.input.agentType).toBe("explore"); + expect(stop!.input.stopReason).toBe("end_turn"); + expect(stop!.input.response).toContain("Hello from subagent test!"); + + expect( + requestHandler.childPromptsWithHookContext.some((prompt) => + prompt.includes(`${CHILD_CONTEXT_PREFIX}Read the file "subagent-test.txt"`) + ), + "the start hook's additionalContext should be prepended to the child's model prompt" + ).toBe(true); + await expect + .poll(() => requestHandler.parentRequestsWithModifiedResponse.length, { + timeout: SUBAGENT_TEST_TIMEOUT_MS, + }) + .toBeGreaterThan(0); + await session.disconnect(); + }, + SUBAGENT_TEST_TIMEOUT_MS + ); }); diff --git a/nodejs/test/e2e/suspend.e2e.test.ts b/nodejs/test/e2e/suspend.e2e.test.ts index 2c8639ad38..64595b2d0c 100644 --- a/nodejs/test/e2e/suspend.e2e.test.ts +++ b/nodejs/test/e2e/suspend.e2e.test.ts @@ -58,11 +58,11 @@ function onTestFinishedStop(client: CopilotClient): void { } describe("Suspend RPC", async () => { - const { copilotClient: client, env, workDir } = await createSdkTestContext(); + const { copilotClient: client, env, workDir, createClient } = await createSdkTestContext(); const SHARED_TOKEN = "suspend-shared-test-token"; function createTcpServer(): CopilotClient { - const server = new CopilotClient({ + const server = createClient({ workingDirectory: workDir, env, gitHubToken: DEFAULT_GITHUB_TOKEN, @@ -76,7 +76,7 @@ describe("Suspend RPC", async () => { } function createConnectingClient(cliUrl: string): CopilotClient { - const connectedClient = new CopilotClient({ + const connectedClient = createClient({ connection: RuntimeConnection.forUri(cliUrl, { connectionToken: SHARED_TOKEN }), }); onTestFinishedStop(connectedClient); diff --git a/nodejs/test/e2e/system_message_sections.e2e.test.ts b/nodejs/test/e2e/system_message_sections.e2e.test.ts index 51380cf4ba..f984d8a9b6 100644 --- a/nodejs/test/e2e/system_message_sections.e2e.test.ts +++ b/nodejs/test/e2e/system_message_sections.e2e.test.ts @@ -2,12 +2,20 @@ * Copyright (c) Microsoft Corporation. All rights reserved. *--------------------------------------------------------------------------------------------*/ -import { describe, expect, it } from "vitest"; -import { approveAll } from "../../src/index.js"; +import { createServer } from "node:http"; +import { text } from "node:stream/consumers"; +import { describe, expect, it, onTestFinished } from "vitest"; +import { + approveAll, + type CopilotSession, + type NamedProviderConfig, + type ProviderModelConfig, +} from "../../src/index.js"; import { createSdkTestContext } from "./harness/sdkTestContext.js"; +import { waitForCondition } from "./harness/sdkTestHelper.js"; describe("System message sections", async () => { - const { copilotClient: client } = await createSdkTestContext(); + const { copilotClient: client, createClient, workDir } = await createSdkTestContext(); it("should_use_replaced_identity_section_in_response", async () => { const session = await client.createSession({ @@ -62,4 +70,160 @@ describe("System message sections", async () => { await session.disconnect(); }); + + it( + "should_move_autopilot_instructions_out_of_a_non_preamble_customized_system_prompt", + { timeout: 90_000 }, + async () => { + let modelRequest: + | { + messages?: Array<{ role?: string; content?: string }>; + } + | undefined; + let modelFailure: Error | undefined; + const modelServer = createServer((request, response) => { + void (async () => { + modelRequest ??= JSON.parse(await text(request)); + response.writeHead(200, { "content-type": "application/json" }); + response.end( + JSON.stringify({ + id: "mode-customization-completion", + object: "chat.completion", + created: 0, + model: "test-model", + choices: [ + { + index: 0, + message: { + role: "assistant", + content: null, + tool_calls: [ + { + id: "mode-customization-task-complete", + type: "function", + function: { + name: "task_complete", + arguments: + '{"summary":"MODE_CUSTOMIZATION_REPRO_OK"}', + }, + }, + ], + }, + finish_reason: "tool_calls", + }, + ], + usage: { prompt_tokens: 10, completion_tokens: 10, total_tokens: 20 }, + }) + ); + })().catch((error: unknown) => { + modelFailure = error instanceof Error ? error : new Error(String(error)); + response.writeHead(500).end(); + }); + }); + const localClient = createClient(); + let session: CopilotSession | undefined; + let serverStart: Promise | undefined; + onTestFinished(async () => { + const errors: unknown[] = []; + if (session) { + try { + await session.disconnect(); + } catch (error) { + errors.push(error); + } + } + try { + errors.push(...(await localClient.stop())); + } catch (error) { + errors.push(error); + } + if (serverStart) { + const [startup] = await Promise.allSettled([serverStart]); + if (startup.status === "rejected") { + errors.push(startup.reason); + } + } + modelServer.closeAllConnections(); + if (modelServer.listening) { + try { + await new Promise((resolve, reject) => { + modelServer.close((error) => (error ? reject(error) : resolve())); + }); + } catch (error) { + errors.push(error); + } + } + if (errors.length) { + throw new AggregateError(errors, "Mode customization fixture cleanup failed"); + } + }); + serverStart = new Promise((resolve, reject) => { + modelServer.once("error", reject); + modelServer.listen(0, "127.0.0.1", resolve); + }); + await serverStart; + const address = modelServer.address(); + if (!address || typeof address === "string") { + throw new Error("Missing local model server address"); + } + + const providers: NamedProviderConfig[] = [ + { + name: "local", + type: "openai", + baseUrl: `http://127.0.0.1:${address.port}`, + apiKey: "test", + wireApi: "completions", + }, + ]; + const models: ProviderModelConfig[] = [ + { id: "model", provider: "local", modelId: "test-model", wireModel: "test-model" }, + ]; + session = await localClient.createSession({ + onPermissionRequest: approveAll, + workingDirectory: workDir, + providers, + models, + model: "local/model", + systemMessage: { + mode: "customize", + sections: { + identity: { + action: "replace", + content: "You are a customized coding assistant.", + }, + }, + }, + }); + + await session.rpc.mode.set({ mode: "autopilot" }); + await session.send({ + prompt: "Call task_complete with summary MODE_CUSTOMIZATION_REPRO_OK.", + }); + await waitForCondition(() => modelRequest !== undefined, { + timeoutMs: 30_000, + timeoutMessage: "The local model did not receive the autopilot request", + }); + await session.abort(); + + if (modelFailure) { + throw modelFailure; + } + expect(modelRequest).toBeDefined(); + const systemMessage = getMessage(modelRequest!, "system"); + const userMessage = getMessage(modelRequest!, "user"); + expect(systemMessage).toContain("You are a customized coding assistant."); + expect(systemMessage).not.toContain(""); + expect(userMessage).toContain(""); + expect(userMessage).toContain(""); + } + ); }); + +function getMessage( + request: { messages?: Array<{ role?: string; content?: string }> }, + role: "system" | "user" +): string | undefined { + const message = request.messages?.find((candidate) => candidate.role === role); + return message?.content; +} diff --git a/nodejs/test/e2e/system_message_transform.e2e.test.ts b/nodejs/test/e2e/system_message_transform.e2e.test.ts index ef37c39e9a..55af45fef9 100644 --- a/nodejs/test/e2e/system_message_transform.e2e.test.ts +++ b/nodejs/test/e2e/system_message_transform.e2e.test.ts @@ -54,7 +54,8 @@ describe("System message transform", async () => { }); it("should apply transform modifications to section content", async () => { - const session = await client.createSession({ + let lastInstructions = ""; + await using session = await client.createSession({ onPermissionRequest: approveAll, systemMessage: { mode: "customize", @@ -64,22 +65,37 @@ describe("System message transform", async () => { return content + "\nTRANSFORM_MARKER"; }, }, + last_instructions: { + action: (content) => { + lastInstructions = content; + return content.replace( + /\n*\n[\s\S]*?<\/subagent_model_preferences>/g, + "" + ); + }, + }, }, }, }); + await session.rpc.tools.updateSubagentSettings({ + subagents: { agents: { "general-purpose": { model: "claude-haiku-4.5" } } }, + }); await writeFile(join(workDir, "hello.txt"), "Hello!"); - await session.sendAndWait({ + const assistantMessage = await session.sendAndWait({ prompt: "Read the contents of hello.txt", }); + expect(assistantMessage?.type).toBe("assistant.message"); // Verify the transform result was actually applied to the system message const traffic = await openAiEndpoint.getExchanges(); const systemMessage = getSystemMessage(traffic[0]); expect(systemMessage).toContain("TRANSFORM_MARKER"); - - await session.disconnect(); + expect(lastInstructions.match(/\n/g)).toHaveLength(1); + expect(lastInstructions).toContain("- general-purpose: claude-haiku-4.5"); + expect(systemMessage).not.toContain("\n"); + expect(systemMessage).not.toContain("- general-purpose: claude-haiku-4.5"); }); it("should work with static overrides and transforms together", async () => { diff --git a/nodejs/test/e2e/telemetry-skills.e2e.test.ts b/nodejs/test/e2e/telemetry-skills.e2e.test.ts new file mode 100644 index 0000000000..92dbc80f64 --- /dev/null +++ b/nodejs/test/e2e/telemetry-skills.e2e.test.ts @@ -0,0 +1,470 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +import { mkdir, readFile, rename, writeFile } from "fs/promises"; +import { join } from "path"; +import { fileURLToPath } from "url"; +import { describe, expect, it } from "vitest"; +import { approveAll, RuntimeConnection } from "../../src/index.js"; +import { createSdkTestContext } from "./harness/sdkTestContext.js"; + +interface Span { + type: string; + name: string; + traceId: string; + spanId: string; + parentSpanId: string; + kind: number; + attributes: Record; + status: { code: number }; + events: { name: string; attributes: Record }[]; +} + +async function spansFrom(path: string): Promise { + const lines = (await readFile(path, "utf8")).split("\n").filter((line) => line.trim()); + return lines.map((line): Span => JSON.parse(line)).filter((entry) => entry.type === "span"); +} + +async function createSkill( + workDir: string, + directory = "skills", + name = "review", + description = "OTel skill description sentinel" +): Promise { + const root = join(workDir, directory, name); + await mkdir(join(root, "references"), { recursive: true }); + await mkdir(join(root, "scripts"), { recursive: true }); + await writeFile( + join(root, "SKILL.md"), + `---\nname: ${name}\ndescription: ${description}\n---\nFollow the user's explicit instructions. The verification word is OTEL_SKILL_ORCHID.\n` + ); + await writeFile(join(root, "references", "policy.txt"), "OTEL_RESOURCE_SAPPHIRE"); + await writeFile( + join(root, "scripts", "check.cjs"), + 'console.log("OTEL_SCRIPT_AMBER"); process.exit(0);\n' + ); + return root; +} + +function expectNoLegacySkillFields(attributes: Record): void { + for (const name of [ + "github.copilot.tool.parameters.skill_name", + "github.copilot.skill.name", + "github.copilot.skill.path", + ]) { + expect(attributes).not.toHaveProperty(name); + } +} + +const shell = process.platform === "win32" ? "powershell" : "bash"; + +function expectShellLauncher(span: Span): void { + expect(span.attributes["process.executable.name"]).toMatch( + process.platform === "win32" ? /^(pwsh|powershell)(\.exe)?$/i : /^bash$/ + ); +} + +for (const captureContent of [false, true]) { + describe(`Skill telemetry capture ${captureContent}`, async () => { + const { createClient, workDir } = await createSdkTestContext({ + copilotClientOptions: { + connection: RuntimeConnection.forStdio(), + }, + }); + + it( + `exports loader resource and script facts capture ${captureContent}`, + { timeout: 90_000 }, + async () => { + const telemetryFile = "loader-telemetry.jsonl"; + await using client = createClient({ + telemetry: { exporterType: "file", filePath: telemetryFile, captureContent }, + }); + const root = await createSkill(workDir); + await writeFile(join(workDir, "policy.txt"), "OTEL_UNRELATED_POLICY"); + const session = await client.createSession({ + enableConfigDiscovery: false, + skillDirectories: [join(workDir, "skills")], + onPermissionRequest: approveAll, + }); + const calls: { id: string; tool: string }[] = []; + const exits = new Map(); + session.on("tool.execution_start", (event) => { + calls.push({ id: event.data.toolCallId, tool: event.data.toolName }); + }); + session.on("tool.execution_complete", (event) => { + const exit = event.data.shellExecution?.exitCode; + if (exit !== undefined) { + exits.set(event.data.toolCallId, exit); + } + }); + const response = await session.sendAndWait( + { + prompt: + "Use the skill tool to load review. Then use view to read skills/review/references/policy.txt. " + + "Then use the shell tool in sync mode to run exactly: node './skills/review/scripts/check.cjs'. " + + "Then use view to read policy.txt in the working directory, outside the skill. " + + "Then use view to read skills/review/SKILL.md, and use view to list the skills/review directory. " + + "After all six calls finish, reply with exactly OTEL_SKILL_FLOW_DONE.", + }, + 90_000 + ); + expect(response?.data.content).toContain("OTEL_SKILL_FLOW_DONE"); + await session.disconnect(); + expect(await client.stop()).toEqual([]); + + const spans = await spansFrom(join(workDir, telemetryFile)); + const tools = spans.filter( + (span) => span.attributes["gen_ai.operation.name"] === "execute_tool" + ); + expect(tools).toHaveLength(6); + expect(tools.every((span) => span.kind === 0)).toBe(true); + expect(calls.filter((call) => call.tool === "skill")).toHaveLength(1); + for (const call of calls) { + expect( + tools.filter((span) => span.attributes["gen_ai.tool.call.id"] === call.id) + ).toHaveLength(1); + } + const loader = tools.find( + (span) => span.attributes["gen_ai.tool.name"] === "skill" + )!; + expect(loader.name).toBe("execute_tool skill review"); + expect(loader.attributes["gen_ai.skill.name"]).toBe("review"); + const reader = tools.find( + (span) => + span.attributes["gen_ai.skill.resource.name"] === "references/policy.txt" + )!; + expect(reader.name).toBe("execute_tool view review references/policy.txt"); + const command = tools.find( + (span) => span.attributes["gen_ai.skill.resource.name"] === "scripts/check.cjs" + )!; + expect(command.name).toBe(`execute_tool ${shell} review scripts/check.cjs`); + expect(command.attributes["process.exit.code"]).toBe(0); + expect(command.attributes["process.exit.code"]).toBe( + exits.get(String(command.attributes["gen_ai.tool.call.id"])) + ); + expectShellLauncher(command); + expect(reader.parentSpanId).toBe(loader.parentSpanId); + expect(command.parentSpanId).toBe(loader.parentSpanId); + expect(reader.traceId).toBe(loader.traceId); + expect(command.traceId).toBe(loader.traceId); + const unrelated = tools.filter( + (span) => span.attributes["gen_ai.tool.name"] === "view" && span !== reader + ); + expect(unrelated).toHaveLength(3); + for (const generic of unrelated) { + expect(generic.name).toBe("execute_tool view"); + expect(generic.attributes).not.toHaveProperty("gen_ai.skill.name"); + expect(generic.attributes).not.toHaveProperty("gen_ai.skill.resource.name"); + } + for (const span of [loader, reader, command]) { + expectNoLegacySkillFields(span.attributes); + expect(span.attributes["gen_ai.operation.name"]).toBe("execute_tool"); + if (captureContent) { + expect(span.attributes["gen_ai.skill.description"]).toBe( + "OTel skill description sentinel" + ); + expect( + fileURLToPath(String(span.attributes["gen_ai.skill.source.uri"])) + ).toBe(join(root, "SKILL.md")); + } else { + expect(span.attributes).not.toHaveProperty("gen_ai.skill.description"); + expect(span.attributes).not.toHaveProperty("gen_ai.skill.source.uri"); + expect(span.attributes).not.toHaveProperty("process.executable.path"); + expect(span.attributes).not.toHaveProperty("gen_ai.tool.call.arguments"); + expect(span.attributes).not.toHaveProperty("gen_ai.tool.call.result"); + } + } + for (const span of spans) { + expectNoLegacySkillFields(span.attributes); + for (const event of span.events) { + expectNoLegacySkillFields(event.attributes); + } + } + const invocation = spans + .flatMap((span) => span.events) + .find((event) => event.name === "github.copilot.skill.invoked")!; + expect(invocation.attributes["gen_ai.skill.name"]).toBe("review"); + expectNoLegacySkillFields(invocation.attributes); + expect(Boolean(invocation.attributes["github.copilot.skill.content"])).toBe( + captureContent + ); + if (captureContent) { + expect(invocation.attributes["gen_ai.skill.description"]).toBe( + "OTel skill description sentinel" + ); + expect( + fileURLToPath(String(invocation.attributes["gen_ai.skill.source.uri"])) + ).toBe(join(root, "SKILL.md")); + } else { + expect(invocation.attributes).not.toHaveProperty("gen_ai.skill.description"); + expect(invocation.attributes).not.toHaveProperty("gen_ai.skill.source.uri"); + } + } + ); + + it( + `exports unknown skill and ordinary zero and nonzero exits capture ${captureContent}`, + { timeout: 90_000 }, + async () => { + const telemetryFile = "exit-telemetry.jsonl"; + await using client = createClient({ + telemetry: { exporterType: "file", filePath: telemetryFile, captureContent }, + }); + await createSkill(workDir); + await writeFile( + join(workDir, "exit.cjs"), + "process.exit(Number(process.argv[2]));\n" + ); + const session = await client.createSession({ + enableConfigDiscovery: false, + skillDirectories: [join(workDir, "skills")], + onPermissionRequest: approveAll, + }); + const exits = new Map(); + session.on("tool.execution_complete", (event) => { + const exit = event.data.shellExecution?.exitCode; + if (exit !== undefined) { + exits.set(event.data.toolCallId, exit); + } + }); + const response = await session.sendAndWait( + { + prompt: + "Use the skill tool to attempt to load otel-nonexistent. Do not search for or install it. " + + "Then use the shell tool in sync mode to run exactly: node './exit.cjs' 0; exit 0. " + + "Then use the shell tool in sync mode to run exactly: node './exit.cjs' 7; exit 7. " + + "A nonzero exit is intentional. Do not retry it. Reply with exactly OTEL_EXIT_FLOW_DONE.", + }, + 90_000 + ); + expect(response?.data.content).toContain("OTEL_EXIT_FLOW_DONE"); + await session.disconnect(); + expect(await client.stop()).toEqual([]); + const spans = await spansFrom(join(workDir, telemetryFile)); + const loader = spans.find( + (span) => span.attributes["gen_ai.tool.name"] === "skill" + )!; + expect(loader.name).toBe("execute_tool skill otel-nonexistent"); + expect(loader.attributes["gen_ai.skill.name"]).toBe("otel-nonexistent"); + expect(loader.attributes).not.toHaveProperty("gen_ai.skill.description"); + expect(loader.attributes).not.toHaveProperty("gen_ai.skill.source.uri"); + expect(loader.status.code).toBe(2); + const commands = spans.filter( + (span) => span.attributes["gen_ai.tool.name"] === shell + ); + expect(commands).toHaveLength(2); + expect(commands.map((span) => span.attributes["process.exit.code"]).sort()).toEqual( + [0, 7] + ); + for (const command of commands) { + expectShellLauncher(command); + expect(command.name).toBe( + `execute_tool ${shell} ${command.attributes["process.executable.name"]}` + ); + expect(command.attributes).not.toHaveProperty("gen_ai.skill.name"); + expect(command.attributes).not.toHaveProperty("gen_ai.skill.resource.name"); + expect(command.attributes["process.exit.code"]).toBe( + exits.get(String(command.attributes["gen_ai.tool.call.id"])) + ); + expect(command.status.code).not.toBe(2); + } + } + ); + + it( + `keeps reloaded skill provenance and child attribution separate capture ${captureContent}`, + { timeout: 90_000 }, + async () => { + const telemetryFile = "reload-telemetry.jsonl"; + await using client = createClient({ + telemetry: { exporterType: "file", filePath: telemetryFile, captureContent }, + }); + const oldRoot = await createSkill(workDir); + const childRoot = await createSkill(workDir, "skills", "child-review"); + const session = await client.createSession({ + enableConfigDiscovery: false, + skillDirectories: [join(workDir, "skills")], + onPermissionRequest: approveAll, + }); + const childExits = new Map(); + const shellStarts: unknown[] = []; + session.on("tool.execution_start", (event) => { + if (event.data.toolName === shell) { + shellStarts.push(event.data); + } + }); + session.on("tool.execution_complete", (event) => { + const exit = event.data.shellExecution?.exitCode; + if (exit !== undefined) { + childExits.set(event.data.toolCallId, exit); + } + }); + const first = await session.sendAndWait({ + prompt: "Use the skill tool to load review, then reply with exactly OTEL_BEFORE_RELOAD_DONE.", + }); + expect(first?.data.content).toContain("OTEL_BEFORE_RELOAD_DONE"); + const newRoot = join(workDir, "skills", "moved-review"); + await rename(oldRoot, newRoot); + await writeFile( + join(newRoot, "SKILL.md"), + "---\nname: review\ndescription: Reloaded OTel description\n---\nFollow the user's explicit instructions.\n" + ); + const reload = await session.rpc.skills.reload(); + expect(reload.errors).toEqual([]); + const second = await session.sendAndWait( + { + prompt: + "Use the skill tool to load review again. Then use the task tool with agent_type exactly 'task' and mode 'sync' " + + "to load child-review with the skill tool, use view to read skills/child-review/references/policy.txt, " + + "then use the shell tool in sync mode to run exactly: node './skills/child-review/scripts/check.cjs'. " + + "The task agent should report the resource and script words. After the agent finishes, " + + "reply with exactly OTEL_AFTER_RELOAD_DONE.", + }, + 90_000 + ); + expect(second?.data.content).toContain("OTEL_AFTER_RELOAD_DONE"); + await session.disconnect(); + expect(await client.stop()).toEqual([]); + const spans = await spansFrom(join(workDir, telemetryFile)); + const loaders = spans.filter( + (span) => span.attributes["gen_ai.tool.name"] === "skill" + ); + expect(loaders).toHaveLength(3); + expect( + loaders.filter((span) => span.name === "execute_tool skill review") + ).toHaveLength(2); + expect( + loaders.map((span) => ({ + name: span.name, + agent: span.attributes["gen_ai.agent.name"], + })) + ).toContainEqual({ name: "execute_tool skill child-review", agent: "task" }); + const child = loaders.find( + (span) => span.attributes["gen_ai.agent.name"] === "task" + )!; + expect(child).toBeDefined(); + expect(child.name).toBe("execute_tool skill child-review"); + const parent = spans.find((span) => span.spanId === child.parentSpanId)!; + expect(parent.attributes["gen_ai.operation.name"]).toBe("invoke_agent"); + expect(parent.attributes["gen_ai.agent.name"]).toBe("task"); + expect(child.traceId).toBe(parent.traceId); + const childReader = spans.find( + (span) => span.name === "execute_tool view child-review references/policy.txt" + )!; + expect(childReader).toBeDefined(); + expect(childReader.parentSpanId).toBe(parent.spanId); + expect(childReader.traceId).toBe(parent.traceId); + expect(childReader.attributes["gen_ai.agent.name"]).toBe("task"); + const childCommands = spans.filter( + (span) => span.attributes["gen_ai.skill.resource.name"] === "scripts/check.cjs" + ); + expect(childCommands, JSON.stringify(shellStarts)).toHaveLength(1); + const childCommand = childCommands[0]; + expect(childCommand.name).toBe( + `execute_tool ${shell} child-review scripts/check.cjs` + ); + expect(childCommand.parentSpanId).toBe(parent.spanId); + expect(childCommand.traceId).toBe(parent.traceId); + expect(childCommand.attributes["gen_ai.agent.name"]).toBe("task"); + expectShellLauncher(childCommand); + expect(childCommand.attributes["process.exit.code"]).toBe(0); + expect(childCommand.attributes["process.exit.code"]).toBe( + childExits.get(String(childCommand.attributes["gen_ai.tool.call.id"])) + ); + if (captureContent) { + expect( + loaders + .map((span) => + fileURLToPath(String(span.attributes["gen_ai.skill.source.uri"])) + ) + .sort() + ).toEqual( + [ + join(oldRoot, "SKILL.md"), + join(newRoot, "SKILL.md"), + join(childRoot, "SKILL.md"), + ].sort() + ); + expect( + loaders.find( + (span) => + fileURLToPath( + String(span.attributes["gen_ai.skill.source.uri"]) + ) === join(oldRoot, "SKILL.md") + )!.attributes["gen_ai.skill.description"] + ).toBe("OTel skill description sentinel"); + expect( + loaders.find( + (span) => + fileURLToPath( + String(span.attributes["gen_ai.skill.source.uri"]) + ) === join(newRoot, "SKILL.md") + )!.attributes["gen_ai.skill.description"] + ).toBe("Reloaded OTel description"); + } + } + ); + }); +} + +describe("Preloaded skill telemetry", async () => { + const telemetryFile = "preload-telemetry.jsonl"; + const { createClient, workDir } = await createSdkTestContext({ + copilotClientOptions: { + connection: RuntimeConnection.forStdio(), + }, + }); + it("emits an invocation receipt without fabricating an execute tool span", async () => { + await using client = createClient({ + telemetry: { exporterType: "file", filePath: telemetryFile, captureContent: true }, + }); + const root = await createSkill(workDir); + const session = await client.createSession({ + enableConfigDiscovery: false, + skillDirectories: [join(workDir, "skills")], + customAgents: [ + { + name: "preloaded-review", + description: "Preload review", + prompt: "Follow the user.", + skills: ["review"], + }, + ], + agent: "preloaded-review", + onPermissionRequest: approveAll, + }); + const response = await session.sendAndWait({ + prompt: "Do not use any tools. Report the verification word from your preloaded review skill and then OTEL_PRELOAD_DONE.", + }); + expect(response?.data.content).toContain("OTEL_PRELOAD_DONE"); + expect(response?.data.content).toContain("OTEL_SKILL_ORCHID"); + await session.disconnect(); + expect(await client.stop()).toEqual([]); + const spans = await spansFrom(join(workDir, telemetryFile)); + expect( + spans.filter((span) => span.attributes["gen_ai.operation.name"] === "execute_tool") + ).toHaveLength(0); + const agent = spans.find( + (span) => span.attributes["gen_ai.operation.name"] === "invoke_agent" + )!; + expect(agent.attributes["gen_ai.agent.name"]).toBe("preloaded-review"); + const invocations = agent.events.filter( + (event) => event.name === "github.copilot.skill.invoked" + ); + expect(invocations).toHaveLength(1); + const invocation = invocations[0]; + expect(invocation.attributes["gen_ai.skill.name"]).toBe("review"); + expect(invocation.attributes["gen_ai.skill.description"]).toBe( + "OTel skill description sentinel" + ); + expect(fileURLToPath(String(invocation.attributes["gen_ai.skill.source.uri"]))).toBe( + join(root, "SKILL.md") + ); + expect(invocation.attributes["github.copilot.skill.content"]).toContain( + "OTEL_SKILL_ORCHID" + ); + expectNoLegacySkillFields(invocation.attributes); + }); +}); diff --git a/nodejs/test/e2e/telemetry.e2e.test.ts b/nodejs/test/e2e/telemetry.e2e.test.ts index ddcae643e3..49d7e66a26 100644 --- a/nodejs/test/e2e/telemetry.e2e.test.ts +++ b/nodejs/test/e2e/telemetry.e2e.test.ts @@ -7,7 +7,7 @@ import { join } from "path"; import { describe, expect, it } from "vitest"; import { z } from "zod"; import { approveAll, defineTool, RuntimeConnection } from "../../src/index.js"; -import { createSdkTestContext } from "./harness/sdkTestContext.js"; +import { createSdkTestContext, DEFAULT_GITHUB_TOKEN } from "./harness/sdkTestContext.js"; interface TelemetryEntry { type?: string; @@ -61,6 +61,7 @@ describe("Telemetry export", async () => { workDir, } = await createSdkTestContext({ copilotClientOptions: { + gitHubToken: DEFAULT_GITHUB_TOKEN, // Telemetry is lowered to environment variables the native runtime reads, which // the in-process transport cannot carry per-client (the runtime runs in the shared // host process); see https://github.com/github/copilot-sdk/issues/1934. Pin the @@ -77,6 +78,8 @@ describe("Telemetry export", async () => { }); it("should export file telemetry for sdk interactions", { timeout: 90_000 }, async () => { + await client.start(); + expect((await client.getAuthStatus()).isAuthenticated).toBe(true); const session = await client.createSession({ onPermissionRequest: approveAll, tools: [ @@ -88,6 +91,11 @@ describe("Telemetry export", async () => { ], }); + // Resolve account metadata before the first invocation snapshots its identity. + expect(await session.rpc.gitHubAuth.getStatus()).toMatchObject({ + isAuthenticated: true, + login: "e2e-test-user", + }); const assistantMessage = await session.sendAndWait({ prompt }, 90_000); expect(assistantMessage).toBeDefined(); expect(assistantMessage?.data.content ?? "").toContain("TELEMETRY_E2E_DONE"); @@ -117,6 +125,10 @@ describe("Telemetry export", async () => { session.sessionId ); expect(isRootSpan(invokeAgentSpan!)).toBe(true); + expect(getStringAttribute(invokeAgentSpan!, "enduser.pseudo.id")).toBe( + "e2e-test-tracking-id" + ); + expect(invokeAgentSpan!.attributes).not.toHaveProperty("user.name"); const invokeAgentSpanId = invokeAgentSpan!.spanId; expect(invokeAgentSpanId).toBeTruthy(); const invokeAgentTraceId = invokeAgentSpan!.traceId; @@ -135,6 +147,23 @@ describe("Telemetry export", async () => { (getStringAttribute(span, "gen_ai.input.messages") ?? "").includes(prompt) ) ).toBe(true); + + const toolDefinitions = chatSpans.flatMap((span) => { + const definitions = getStringAttribute(span, "gen_ai.tool.definitions"); + return definitions ? (JSON.parse(definitions) as unknown[]) : []; + }); + expect(toolDefinitions).toContainEqual( + expect.objectContaining({ + name: toolName, + description: "Echoes a marker string for telemetry validation.", + parameters: expect.objectContaining({ + type: "object", + properties: expect.objectContaining({ + value: expect.objectContaining({ type: "string" }), + }), + }), + }) + ); expect( chatSpans.some((span) => (getStringAttribute(span, "gen_ai.output.messages") ?? "").includes( @@ -166,6 +195,7 @@ describe("Telemetry export", async () => { const subagentClient = createClient({ connection: RuntimeConnection.forStdio(), + gitHubToken: DEFAULT_GITHUB_TOKEN, telemetry: { filePath: telemetryFileName, exporterType: "file", @@ -175,9 +205,12 @@ describe("Telemetry export", async () => { }); let sessionId: string; try { + await subagentClient.start(); + expect((await subagentClient.getAuthStatus()).isAuthenticated).toBe(true); const session = await subagentClient.createSession({ onPermissionRequest: approveAll }); sessionId = session.sessionId; try { + expect((await session.rpc.gitHubAuth.getStatus()).isAuthenticated).toBe(true); const response = await session.sendAndWait({ prompt }, 90_000); expect(response?.data.content ?? "").toContain("SUBAGENT_OTEL_DONE"); } finally { @@ -196,6 +229,10 @@ describe("Telemetry export", async () => { (span) => getStringAttribute(span, "gen_ai.operation.name") === "invoke_agent" ); expect(invocationSpans).toHaveLength(2); + for (const span of invocationSpans) { + expect(getStringAttribute(span, "enduser.pseudo.id")).toBe("e2e-test-tracking-id"); + expect(span.attributes).not.toHaveProperty("user.name"); + } const roots = invocationSpans.filter(isRootSpan); expect(roots).toHaveLength(1); const root = roots[0]!; diff --git a/nodejs/test/e2e/tools.e2e.test.ts b/nodejs/test/e2e/tools.e2e.test.ts index 16db1aca87..78d18c0ca9 100644 --- a/nodejs/test/e2e/tools.e2e.test.ts +++ b/nodejs/test/e2e/tools.e2e.test.ts @@ -2,7 +2,7 @@ * Copyright (c) Microsoft Corporation. All rights reserved. *--------------------------------------------------------------------------------------------*/ -import { writeFile } from "fs/promises"; +import { access, writeFile } from "fs/promises"; import { join } from "path"; import { assert, describe, expect, it } from "vitest"; import { z } from "zod"; @@ -45,6 +45,51 @@ describe("Custom tools", async () => { expect(assistantMessage?.data.content).toContain("HELLO"); }); + it("updates the calling connection's tool definitions through the public session RPC", async () => { + const original = { + name: "sdk_epoch_original", + description: "The original SDK-owned tool", + parameters: { type: "object", properties: {} }, + defer: "never", + } as const; + const replacement = { + name: "sdk_epoch_replacement", + description: "A replacement SDK-owned tool", + parameters: { type: "object", properties: {} }, + defer: "never", + } as const; + const session = await client.createSession({ + onPermissionRequest: approveAll, + tools: [ + defineTool(original.name, { + description: original.description, + handler: () => "ORIGINAL_TOOL_RESULT", + }), + ], + }); + try { + const currentToolNames = async () => { + await session.rpc.tools.initializeAndValidate(); + return (await session.rpc.tools.getCurrentMetadata()).tools.map( + (tool) => tool.name + ); + }; + expect(await currentToolNames()).toContain(original.name); + + await session.rpc.tools.set({ tools: [original, replacement] }); + expect(await currentToolNames()).toEqual( + expect.arrayContaining([original.name, replacement.name]) + ); + + await session.rpc.tools.set({ tools: [replacement] }); + const afterRemoval = await currentToolNames(); + expect(afterRemoval).toContain(replacement.name); + expect(afterRemoval).not.toContain(original.name); + } finally { + await session.disconnect(); + } + }); + // TODO(cli-1.0.81-2): CLI 1.0.81-2 stops servicing nested requests on the same stdio // connection while awaiting a tool handler, so calling session.history.clearContext // from inside a terminal tool deadlocks. Tracked as a runtime reentrancy regression; @@ -299,6 +344,104 @@ describe("Custom tools", async () => { expect(assistantMessage?.data.content?.toLowerCase()).toMatch(/hello|search|found/); }); + it("string schema apply_patch override binds patch input", async () => { + const patch = + "*** Begin Patch\n*** Add File: override-marker.txt\n+from-native\n*** End Patch"; + let receivedPatch: string | undefined; + let receivedArguments: unknown; + const session = await client.createSession({ + onPermissionRequest: approveAll, + tools: [ + defineTool("apply_patch", { + parameters: z.string(), + overridesBuiltInTool: true, + handler: (input, invocation) => { + receivedPatch = input; + receivedArguments = invocation.arguments; + return "HOST_PATCH_HANDLED"; + }, + }), + ], + }); + + const response = await session.sendAndWait({ + prompt: "Use apply_patch to apply the supplied patch.", + }); + + expect(receivedPatch).toBe(patch); + expect(receivedArguments).toBe(patch); + expect(response?.data.content).toContain("Host override completed."); + await expect(access(join(workDir, "override-marker.txt"))).rejects.toMatchObject({ + code: "ENOENT", + }); + const traffic = await openAiEndpoint.getExchanges(); + const tool = traffic[0]?.request.tools?.find( + (entry) => entry.type === "function" && entry.function.name === "apply_patch" + ); + expect(tool?.function.parameters).toMatchObject({ + type: "object", + properties: { input: { type: "string" } }, + required: ["input"], + }); + expect( + traffic[1]?.request.messages.some( + (message) => message.role === "tool" && message.content === "HOST_PATCH_HANDLED" + ) + ).toBe(true); + }); + + it("reports rejected apply_patch string schema to the SDK", async () => { + const warnings: string[] = []; + const session = await client.createSession({ + onPermissionRequest: approveAll, + onEvent: (event) => { + if (event.type === "session.warning") { + warnings.push(event.data.message); + } + }, + tools: [ + defineTool("apply_patch", { + parameters: { + type: "string", + $defs: { patch: { type: "string" } }, + $ref: "#/$defs/patch", + }, + overridesBuiltInTool: true, + handler: () => "unexpected override call", + }), + defineTool("valid_sibling", { + parameters: z.object({}), + handler: () => "valid", + }), + defineTool("invalid_sibling", { + parameters: { type: "number" }, + handler: () => "unexpected invalid tool call", + }), + ], + }); + + await session.rpc.mode.set({ mode: "plan" }); + await session.rpc.mode.set({ mode: "interactive" }); + const response = await session.sendAndWait({ + prompt: "Reply with exactly Session ready.", + }); + expect(response?.data.content).toContain("Session ready."); + expect(warnings).toEqual([ + 'External tool "apply_patch" has invalid parameters schema (apply_patch string overrides cannot contain JSON Schema references), ignoring tool', + ]); + const traffic = await openAiEndpoint.getExchanges(); + expect( + traffic[0]?.request.tools?.some( + (tool) => tool.type === "function" && tool.function.name === "valid_sibling" + ) + ).toBe(true); + expect( + traffic[0]?.request.tools?.some( + (tool) => tool.type === "function" && tool.function.name === "invalid_sibling" + ) + ).toBe(false); + }); + it("denies custom tool when permission denied", async () => { let toolHandlerCalled = false; diff --git a/nodejs/test/e2e/tools_anthropic_updates.e2e.test.ts b/nodejs/test/e2e/tools_anthropic_updates.e2e.test.ts new file mode 100644 index 0000000000..721b432ae1 --- /dev/null +++ b/nodejs/test/e2e/tools_anthropic_updates.e2e.test.ts @@ -0,0 +1,411 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +import { describe, expect, it } from "vitest"; +import { + approveAll, + CopilotRequestHandler, + defineTool, + type CopilotRequestContext, +} from "../../src/index.js"; +import { createSdkTestContext } from "./harness/sdkTestContext.js"; +import { waitForCondition } from "./harness/sdkTestHelper.js"; + +interface AnthropicRequest { + system: unknown; + tools: unknown; + messages: Array<{ role: string; content: unknown }>; +} + +function streamResponse(block: Record, stopReason: string): Response { + const events: Array<[string, Record]> = [ + [ + "message_start", + { + type: "message_start", + message: { + id: "msg_sdk_tool_update", + type: "message", + role: "assistant", + model: "claude-opus-5.5", + content: [], + stop_reason: null, + stop_sequence: null, + usage: { input_tokens: 5, output_tokens: 1 }, + }, + }, + ], + ["content_block_start", { type: "content_block_start", index: 0, content_block: block }], + ["content_block_stop", { type: "content_block_stop", index: 0 }], + [ + "message_delta", + { + type: "message_delta", + delta: { stop_reason: stopReason, stop_sequence: null }, + usage: { output_tokens: 7 }, + }, + ], + ["message_stop", { type: "message_stop" }], + ]; + return new Response( + events + .map(([event, data]) => `event: ${event}\ndata: ${JSON.stringify(data)}\n\n`) + .join(""), + { headers: { "content-type": "text/event-stream" } } + ); +} + +function streamRejectionAfterOutput(): Response { + const events = [ + [ + "message_start", + { + type: "message_start", + message: { + id: "msg_sdk_tool_rejected", + type: "message", + role: "assistant", + model: "claude-opus-5.5", + content: [], + stop_reason: null, + stop_sequence: null, + usage: { input_tokens: 5, output_tokens: 1 }, + }, + }, + ], + [ + "content_block_start", + { + type: "content_block_start", + index: 0, + content_block: { type: "text", text: "" }, + }, + ], + [ + "content_block_delta", + { + type: "content_block_delta", + index: 0, + delta: { type: "text_delta", text: "already streamed" }, + }, + ], + [ + "error", + { + type: "error", + error: { type: "invalid_request_error", message: "invalid tool_addition" }, + }, + ], + ] as const; + return new Response( + events + .map(([event, data]) => `event: ${event}\ndata: ${JSON.stringify(data)}\n\n`) + .join(""), + { headers: { "content-type": "text/event-stream" } } + ); +} + +class AnthropicToolUpdateHandler extends CopilotRequestHandler { + readonly requests: Array<{ body: AnthropicRequest; beta: string | null }> = []; + + constructor( + private readonly rejectPositionedUpdate: false | "before-output" | "after-output" = false + ) { + super(); + } + + protected override async sendRequest( + request: Request, + ctx: CopilotRequestContext + ): Promise { + if (new URL(request.url).hostname !== "api.anthropic.com") { + return super.sendRequest(request, ctx); + } + const body = (await request.json()) as AnthropicRequest; + const serialized = JSON.stringify(body.messages); + if ( + !serialized.includes("SDK_ANTHROPIC_FIRST") && + !serialized.includes("SDK_ANTHROPIC_AFTER") && + !serialized.includes("SDK_ANTHROPIC_FOLLOW_UP") + ) { + return streamResponse({ type: "text", text: "Unrelated request" }, "end_turn"); + } + this.requests.push({ body, beta: request.headers.get("anthropic-beta") }); + + if (serialized.includes("SDK_ANTHROPIC_FOLLOW_UP")) { + return streamResponse({ type: "text", text: "SDK_ANTHROPIC_LEGACY_OK" }, "end_turn"); + } + if ( + body.messages.some( + (message) => + Array.isArray(message.content) && + message.content.some((block: { type?: string }) => block.type === "tool_result") + ) + ) { + return streamResponse({ type: "text", text: "SDK_ANTHROPIC_TOOL_OK" }, "end_turn"); + } + if (serialized.includes("SDK_ANTHROPIC_AFTER")) { + if (this.rejectPositionedUpdate) { + if (serialized.includes('"type":"tool_addition"')) { + if (this.rejectPositionedUpdate === "after-output") { + return streamRejectionAfterOutput(); + } + return new Response( + JSON.stringify({ + error: { + type: "invalid_request_error", + message: "invalid tool_addition", + }, + }), + { status: 400, headers: { "content-type": "application/json" } } + ); + } + return streamResponse( + { type: "text", text: "SDK_ANTHROPIC_RECOVERED" }, + "end_turn" + ); + } + return streamResponse( + { type: "tool_use", id: "toolu_sdk_added", name: "sdk_epoch_added", input: {} }, + "tool_use" + ); + } + return streamResponse({ type: "text", text: "SDK_ANTHROPIC_FIRST_OK" }, "end_turn"); + } +} + +describe("SDK tool updates on direct Anthropic Messages", async () => { + const handler = new AnthropicToolUpdateHandler(); + const { copilotClient: client } = await createSdkTestContext({ + copilotClientOptions: { requestHandler: handler }, + }); + + it("keeps the top-level prefix and executes a newly added inline tool", async () => { + let addedToolCalls = 0; + const session = await client.createSession({ + model: "claude-opus-5.5", + toolSearch: { enabled: false }, + onPermissionRequest: approveAll, + provider: { + type: "anthropic", + baseUrl: "https://api.anthropic.com", + apiKey: "synthetic-provider-key", + modelId: "claude-opus-5.5", + wireModel: "claude-opus-5.5", + }, + tools: [ + defineTool("sdk_epoch_original", { + description: "Original SDK tool", + handler: () => "original", + }), + defineTool("sdk_epoch_added", { + description: "Added SDK tool", + handler: () => { + addedToolCalls++; + return "SDK_ADDED_TOOL_RESULT"; + }, + }), + ], + }); + try { + await session.rpc.tools.initializeAndValidate(); + expect( + (await session.rpc.tools.getCurrentMetadata()).tools.map((tool) => tool.name) + ).toEqual(expect.arrayContaining(["sdk_epoch_original", "sdk_epoch_added"])); + const original = { + name: "sdk_epoch_original", + description: "Original SDK tool", + parameters: { type: "object", properties: {} }, + defer: "never", + } as const; + const added = { + name: "sdk_epoch_added", + description: "Added SDK tool", + parameters: { type: "object", properties: {} }, + defer: "never", + } as const; + await session.rpc.tools.set({ tools: [original] }); + + const first = await session.sendAndWait({ prompt: "SDK_ANTHROPIC_FIRST" }); + expect(first?.data.content).toContain("SDK_ANTHROPIC_FIRST_OK"); + await session.rpc.tools.set({ tools: [original, added] }); + const second = await session.sendAndWait({ + prompt: "SDK_ANTHROPIC_AFTER: use sdk_epoch_added now.", + }); + expect(second?.data.content).toContain("SDK_ANTHROPIC_TOOL_OK"); + expect(addedToolCalls).toBe(1); + + expect(handler.requests).toHaveLength(3); + const [before, update, afterTool] = handler.requests; + expect(update.body.system).toEqual(before.body.system); + expect(update.body.tools).toEqual(before.body.tools); + expect(afterTool.body.system).toEqual(before.body.system); + expect(afterTool.body.tools).toEqual(before.body.tools); + const blocks = update.body.messages.flatMap((message) => + Array.isArray(message.content) ? message.content : [] + ); + expect(blocks).toContainEqual( + expect.objectContaining({ + type: "tool_addition", + tool: expect.objectContaining({ + type: "tool_definition", + definition: expect.objectContaining({ name: "sdk_epoch_added" }), + }), + }) + ); + expect(update.beta).toContain("inline-tools-2026-09-15"); + expect(update.beta).not.toContain("mid-conversation-tool-changes-2026-07-01"); + expect(JSON.stringify(afterTool.body.messages)).toContain("SDK_ADDED_TOOL_RESULT"); + } finally { + await session.disconnect(); + } + }); +}); + +describe("SDK tool updates on direct Anthropic Messages", async () => { + const rejectingHandler = new AnthropicToolUpdateHandler("before-output"); + const { copilotClient: retryClient } = await createSdkTestContext({ + copilotClientOptions: { requestHandler: rejectingHandler }, + }); + + it("retries a rejected inline update once with the full current tools", async () => { + const original = { + name: "sdk_epoch_original", + description: "Original SDK tool", + parameters: { type: "object", properties: {} }, + defer: "never", + } as const; + const added = { + name: "sdk_epoch_added", + description: "Added SDK tool", + parameters: { type: "object", properties: {} }, + defer: "never", + } as const; + const session = await retryClient.createSession({ + model: "claude-opus-5.5", + toolSearch: { enabled: false }, + onPermissionRequest: approveAll, + provider: { + type: "anthropic", + baseUrl: "https://api.anthropic.com", + apiKey: "synthetic-provider-key", + modelId: "claude-opus-5.5", + wireModel: "claude-opus-5.5", + }, + tools: [ + defineTool(original.name, { + description: original.description, + handler: () => "original", + }), + defineTool(added.name, { description: added.description, handler: () => "added" }), + ], + }); + try { + await session.rpc.tools.initializeAndValidate(); + await session.rpc.tools.set({ tools: [original] }); + const first = await session.sendAndWait({ prompt: "SDK_ANTHROPIC_FIRST" }); + expect(first?.data.content).toContain("SDK_ANTHROPIC_FIRST_OK"); + + await session.rpc.tools.set({ tools: [original, added] }); + const recovered = await session.sendAndWait({ + prompt: "SDK_ANTHROPIC_AFTER: add a tool.", + }); + expect(recovered?.data.content).toContain("SDK_ANTHROPIC_RECOVERED"); + expect(rejectingHandler.requests).toHaveLength(3); + const [before, rejected, retry] = rejectingHandler.requests; + expect(rejected.body.system).toEqual(before.body.system); + expect(rejected.body.tools).toEqual(before.body.tools); + expect(JSON.stringify(rejected.body.messages)).toContain('"type":"tool_addition"'); + expect(rejected.beta).toContain("inline-tools-2026-09-15"); + expect(retry.body.system).toEqual(before.body.system); + expect((retry.body.tools as Array<{ name: string }>).map((tool) => tool.name)).toEqual( + expect.arrayContaining([original.name, added.name]) + ); + expect(JSON.stringify(retry.body.messages)).not.toContain('"type":"tool_addition"'); + expect(retry.beta).not.toContain("inline-tools-2026-09-15"); + } finally { + await session.disconnect(); + } + }); +}); + +describe("SDK tool updates after a streamed Anthropic rejection", async () => { + const handler = new AnthropicToolUpdateHandler("after-output"); + const { copilotClient: client } = await createSdkTestContext({ + copilotClientOptions: { requestHandler: handler }, + }); + + it("disables the rejected epoch without replaying output and uses full tools next turn", async () => { + const original = { + name: "sdk_epoch_original", + description: "Original SDK tool", + parameters: { type: "object", properties: {} }, + defer: "never", + } as const; + const added = { + name: "sdk_epoch_added", + description: "Added SDK tool", + parameters: { type: "object", properties: {} }, + defer: "never", + } as const; + const session = await client.createSession({ + model: "claude-opus-5.5", + toolSearch: { enabled: false }, + onPermissionRequest: approveAll, + provider: { + type: "anthropic", + baseUrl: "https://api.anthropic.com", + apiKey: "synthetic-provider-key", + modelId: "claude-opus-5.5", + wireModel: "claude-opus-5.5", + }, + tools: [ + defineTool(original.name, { + description: original.description, + handler: () => "original", + }), + defineTool(added.name, { description: added.description, handler: () => "added" }), + ], + }); + let failedTurnIdle = false; + const unsubscribe = session.on((event) => { + if (event.type === "session.idle" && !event.agentId) { + failedTurnIdle = true; + } + }); + try { + await session.rpc.tools.initializeAndValidate(); + await session.rpc.tools.set({ tools: [original] }); + const first = await session.sendAndWait({ prompt: "SDK_ANTHROPIC_FIRST" }); + expect(first?.data.content).toContain("SDK_ANTHROPIC_FIRST_OK"); + failedTurnIdle = false; + + await session.rpc.tools.set({ tools: [original, added] }); + await expect( + session.sendAndWait({ prompt: "SDK_ANTHROPIC_AFTER: add a tool." }) + ).rejects.toThrow(/invalid tool_addition/); + await waitForCondition(() => failedTurnIdle, { + timeoutMessage: "Rejected SDK tool update did not reach session.idle", + }); + expect(handler.requests).toHaveLength(2); + const [before, rejected] = handler.requests; + expect(rejected.body.system).toEqual(before.body.system); + expect(rejected.body.tools).toEqual(before.body.tools); + expect(JSON.stringify(rejected.body.messages)).toContain('"type":"tool_addition"'); + + const followUp = await session.sendAndWait({ prompt: "SDK_ANTHROPIC_FOLLOW_UP" }); + expect(followUp?.data.content).toContain("SDK_ANTHROPIC_LEGACY_OK"); + expect(handler.requests).toHaveLength(3); + const request = handler.requests[2]; + expect( + (request.body.tools as Array<{ name: string }>).map((tool) => tool.name) + ).toEqual(expect.arrayContaining([original.name, added.name])); + expect(JSON.stringify(request.body.messages)).not.toContain('"type":"tool_addition"'); + expect(request.beta).not.toContain("inline-tools-2026-09-15"); + } finally { + unsubscribe(); + await session.disconnect(); + } + }); +}); diff --git a/nodejs/test/e2e/ui_elicitation.e2e.test.ts b/nodejs/test/e2e/ui_elicitation.e2e.test.ts index 51195206aa..4287b4686b 100644 --- a/nodejs/test/e2e/ui_elicitation.e2e.test.ts +++ b/nodejs/test/e2e/ui_elicitation.e2e.test.ts @@ -10,6 +10,7 @@ import { getLegacyCliPathForTests, isInProcessTransport, } from "./harness/sdkTestContext.js"; +import { waitForCondition } from "./harness/sdkTestHelper.js"; describe("UI Elicitation", async () => { const { copilotClient: client } = await createSdkTestContext(); @@ -29,6 +30,74 @@ describe("UI Elicitation Callback", async () => { const ctx = await createSdkTestContext(); const client = ctx.copilotClient; + it( + "automated SDK ask_user callback settles without trusted human authority", + { timeout: 60_000 }, + async () => { + const events: SessionEvent[] = []; + let receivedMessage: string | undefined; + const session = await client.createSession({ + onPermissionRequest: approveAll, + askUserVariant: "elicitation", + onElicitationRequest: async (request) => { + receivedMessage = request.message; + return { + action: "accept", + content: { answer: "github/st-mcp-poc" }, + }; + }, + }); + const unsubscribe = session.on((event) => { + events.push(event); + }); + try { + const response = await session.sendAndWait({ + prompt: + "Invoke the ask_user tool now. Set message to exactly 'Which repository should I inspect?' " + + "and request an object with one required string field named answer. Do not describe the tool call. " + + "After the tool returns, reply with exactly SDK_AUTOMATED_ASK_USER_DONE.", + }); + await waitForCondition( + () => + events.some((event) => event.type === "elicitation.completed") && + events.some((event) => event.type === "tool.execution_complete"), + { + timeoutMs: 60_000, + timeoutMessage: `Timed out waiting for ask_user completion; events: ${events + .map((event) => event.type) + .join(", ")}`, + } + ); + const elicitationEvent = events.find( + (event) => event.type === "elicitation.completed" + ); + const toolEvent = events.find((event) => event.type === "tool.execution_complete"); + + expect(receivedMessage).toBe("Which repository should I inspect?"); + expect(elicitationEvent).toMatchObject({ + type: "elicitation.completed", + data: { + action: "accept", + content: { answer: "github/st-mcp-poc" }, + }, + }); + expect(toolEvent).toMatchObject({ + type: "tool.execution_complete", + data: { success: true }, + }); + expect(events.some((event) => event.type === "elicitation.requested")).toBe(true); + expect(events.some((event) => event.type === "elicitation.completed")).toBe(true); + expect(events.some((event) => event.type === "human_response.recorded")).toBe( + false + ); + expect(response?.data.content).toBe("SDK_AUTOMATED_ASK_USER_DONE"); + } finally { + unsubscribe(); + await session.disconnect(); + } + } + ); + it( "session created with onElicitationRequest reports elicitation capability", { timeout: 60_000 }, diff --git a/nodejs/test/external-tool-cancellation.test.ts b/nodejs/test/external-tool-cancellation.test.ts index 35d15bd8e3..d68e5f5bac 100644 --- a/nodejs/test/external-tool-cancellation.test.ts +++ b/nodejs/test/external-tool-cancellation.test.ts @@ -1,8 +1,89 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + /* eslint-disable @typescript-eslint/no-explicit-any */ import { expect, it, vi } from "vitest"; +import { z } from "zod"; import { CopilotSession } from "../src/session.js"; +import { defineTool } from "../src/types.js"; import type { ToolInvocation } from "../src/types.js"; +it.each([ + ["custom", "*** Begin Patch\n*** End Patch"], + ["function", { input: "*** Begin Patch\n*** End Patch" }], +])( + "passes the patch string to a typed apply_patch override from a %s call", + async (_kind, args) => { + const sendRequest = vi.fn().mockResolvedValue(undefined); + const session = new CopilotSession("session-1", { sendRequest } as never); + const patchHandler = vi.fn((patch: string) => patch.trim()); + session.registerTools([ + defineTool("apply_patch", { + parameters: z.string(), + overridesBuiltInTool: true, + handler: patchHandler, + }), + ]); + + (session as any)._handleBroadcastEvent({ + type: "external_tool.requested", + data: { + requestId: "request-patch", + sessionId: "session-1", + toolCallId: "tool-call-patch", + toolName: "apply_patch", + arguments: args, + }, + }); + + await vi.waitFor(() => expect(sendRequest).toHaveBeenCalledTimes(1)); + expect(patchHandler).toHaveBeenCalledWith( + "*** Begin Patch\n*** End Patch", + expect.objectContaining({ arguments: args }) + ); + expect(sendRequest).toHaveBeenCalledWith( + "session.tools.handlePendingToolCall", + expect.objectContaining({ result: "*** Begin Patch\n*** End Patch" }) + ); + } +); + +it.each([null, 42, {}, { input: 42 }])( + "reports invalid arguments for a string-schema apply_patch override: %j", + async (args) => { + const sendRequest = vi.fn().mockResolvedValue(undefined); + const session = new CopilotSession("session-1", { sendRequest } as never); + const handler = vi.fn((patch: string) => patch.trim()); + session.registerTools([ + defineTool("apply_patch", { + parameters: z.string(), + overridesBuiltInTool: true, + handler, + }), + ]); + + (session as any)._handleBroadcastEvent({ + type: "external_tool.requested", + data: { + requestId: "request-invalid-patch", + sessionId: "session-1", + toolCallId: "tool-call-invalid-patch", + toolName: "apply_patch", + arguments: args, + }, + }); + + await vi.waitFor(() => expect(sendRequest).toHaveBeenCalledTimes(1)); + expect(handler).not.toHaveBeenCalled(); + expect(sendRequest).toHaveBeenCalledWith("session.tools.handlePendingToolCall", { + sessionId: "session-1", + requestId: "request-invalid-patch", + error: "apply_patch string override requires a string input", + }); + } +); + it("cancels a blocked external tool when completion arrives", async () => { const session = new CopilotSession("session-1", {} as never); let invocation: ToolInvocation | undefined; @@ -44,6 +125,34 @@ it("cancels a blocked external tool when completion arrives", async () => { expect(invocation?.signal?.aborted).toBe(true); }); +it("preserves object arguments for an object-schema apply_patch override", async () => { + const sendRequest = vi.fn().mockResolvedValue(undefined); + const session = new CopilotSession("session-1", { sendRequest } as never); + const handler = vi.fn(({ input }: { input: string }) => input); + session.registerTools([ + defineTool("apply_patch", { + parameters: z.object({ input: z.string() }), + overridesBuiltInTool: true, + handler, + }), + ]); + const argumentsObject = { input: "*** Begin Patch\n*** End Patch" }; + + (session as any)._handleBroadcastEvent({ + type: "external_tool.requested", + data: { + requestId: "request-object", + sessionId: "session-1", + toolCallId: "tool-call-object", + toolName: "apply_patch", + arguments: argumentsObject, + }, + }); + + await vi.waitFor(() => expect(sendRequest).toHaveBeenCalledTimes(1)); + expect(handler).toHaveBeenCalledWith(argumentsObject, expect.anything()); +}); + it("does not respond when a cancelled handler returns a late result", async () => { const sendRequest = vi.fn().mockResolvedValue(undefined); const session = new CopilotSession("session-1", { sendRequest } as never); diff --git a/nodejs/test/package-sdk.test.ts b/nodejs/test/package-sdk.test.ts index 1b9f38bee3..cabbe09300 100644 --- a/nodejs/test/package-sdk.test.ts +++ b/nodejs/test/package-sdk.test.ts @@ -57,13 +57,16 @@ describe("release packaging", () => { join(nodeRoot, "node_modules"), "junction" ); + const { files } = JSON.parse( + readFileSync(join(sourceNodeRoot, "package.json"), "utf8") + ) as { files: string[] }; const manifest = JSON.stringify({ name: "@github/copilot-sdk", version: "0.0.0-packaging-test", type: "module", repository: "https://github.com/github/copilot-sdk.git", dependencies: {}, - files: ["dist"], + files, scripts: { "pack:release": "tsx scripts/package-sdk.ts", "verify:release-packages": "tsx scripts/verify-release-packages.ts", @@ -72,6 +75,8 @@ describe("release packaging", () => { write(join(nodeRoot, "package.json"), manifest); write(join(nodeRoot, "dist/index.js"), "export {};"); write(join(nodeRoot, "dist/cjs/index.js"), "module.exports = {};"); + write(join(nodeRoot, "dist/index.d.ts"), "export {};"); + write(join(nodeRoot, "dist/tsconfig.tsbuildinfo"), "local build metadata"); const environment = { ...process.env }; for (const key of [ @@ -162,6 +167,15 @@ globalThis.fetch = async (url) => { expect( readFileSync(join(unpacked, "package/prebuilds/linux-x64/runtime.node"), "utf8") ).toBe(`${source} runtime: linux-x64`); + const sdkUnpacked = join(root, "unpacked-sdk"); + mkdirSync(sdkUnpacked); + await extractTar({ + file: join(nodeRoot, "github-copilot-sdk-0.0.0-packaging-test.tgz"), + cwd: sdkUnpacked, + }); + expect(existsSync(join(sdkUnpacked, "package/dist/index.js"))).toBe(true); + expect(existsSync(join(sdkUnpacked, "package/dist/index.d.ts"))).toBe(true); + expect(existsSync(join(sdkUnpacked, "package/dist/tsconfig.tsbuildinfo"))).toBe(false); rmSync(join(packageRoots[0], "copilot-sdk/extension.js")); const incompletePack = run("pack:release"); diff --git a/nodejs/test/runtimeArtifacts.test.ts b/nodejs/test/runtimeArtifacts.test.ts index b59d43de82..9b00a59f09 100644 --- a/nodejs/test/runtimeArtifacts.test.ts +++ b/nodejs/test/runtimeArtifacts.test.ts @@ -172,6 +172,7 @@ describe("materializeRuntimeBundle", () => { mkdirSync(join(sourceDir, "preloads"), { recursive: true }); writeFileSync(join(sourceDir, "preloads", "extension_bootstrap.mjs"), "bootstrap"); writeFileSync(join(sourceDir, "app.js"), "excluded"); + writeFileSync(join(sourceDir, "cli-main.js"), "excluded"); writeFileSync(join(sourceDir, "copilot"), "excluded"); writeFileSync(join(sourceDir, "copilot.exe"), "excluded"); writeFileSync(join(sourceDir, "LICENSE.md"), "excluded"); @@ -206,6 +207,7 @@ describe("materializeRuntimeBundle", () => { "bootstrap" ); expect(existsSync(join(installDir, "app.js"))).toBe(false); + expect(existsSync(join(installDir, "cli-main.js"))).toBe(false); expect(existsSync(join(installDir, "copilot"))).toBe(false); expect(existsSync(join(installDir, "copilot.exe"))).toBe(false); expect(existsSync(join(installDir, "LICENSE.md"))).toBe(false); diff --git a/nodejs/test/rust-codegen.test.ts b/nodejs/test/rust-codegen.test.ts index 3087dc2c11..17fc4eea47 100644 --- a/nodejs/test/rust-codegen.test.ts +++ b/nodejs/test/rust-codegen.test.ts @@ -13,22 +13,9 @@ import { generateApiTypesCode, generateRpcCode, generateSessionEventsCode, - isRustCodegenEntrypoint, } from "../../scripts/codegen/rust.ts"; import { legacyRequestSchema } from "./legacy-parameters-fixture.ts"; -describe("Rust codegen entrypoint", () => { - it("matches Windows paths case-insensitively", () => { - expect( - isRustCodegenEntrypoint( - "C:\\a\\copilot-agent-runtime\\src\\sdk\\scripts\\codegen\\rust.ts", - "c:\\a\\copilot-agent-runtime\\src\\sdk\\scripts\\codegen\\rust.ts", - "win32" - ) - ).toBe(true); - }); -}); - describe("Rust API type codegen", () => { it.each([ { @@ -70,6 +57,25 @@ describe("Rust API type codegen", () => { expect(code).not.toContain("pub struct SessionAccountResult {"); } ); + it("preserves future customization reload enum values in both directions", () => { + const code = generateApiTypesCode({ + definitions: { + CustomizationReloadStatus: { type: "string", enum: ["reloaded", "skipped"] }, + CustomizationReloadSubsystem: { type: "string", enum: ["skills", "hooks"] }, + ClosedStatus: { type: "string", enum: ["ready"] }, + }, + } as ApiSchema); + + for (const name of ["CustomizationReloadStatus", "CustomizationReloadSubsystem"]) { + expect(code).toContain(`pub enum ${name} {`); + expect(code).toContain(`impl From for ${name} {`); + expect(code).toContain(`impl From<${name}> for String {`); + } + expect(code).toContain('#[serde(from = "String", into = "String")]'); + expect(code).toContain("Unknown(String),"); + expect(code).toContain("pub enum ClosedStatus {"); + expect(code).toContain("#[serde(other)]"); + }); it.each(["anyOf", "oneOf"] as const)( "keeps required phase results typed through %s references", @@ -317,6 +323,54 @@ pub struct Container`); expect(code).toContain("False(UnsupportedEnqueueCommandResult)"); }); + it("reuses the get-sources result for instructions reload without generating an unused type", () => { + const schema = { + definitions: { + InstructionsGetSourcesResult: { + type: "object", + properties: { sources: { type: "array", items: { type: "string" } } }, + required: ["sources"], + }, + OtherResult: { + type: "object", + properties: { sources: { type: "array", items: { type: "string" } } }, + required: ["sources"], + }, + }, + session: { + instructions: { + getSources: { + rpcMethod: "session.instructions.getSources", + params: null, + result: { $ref: "#/definitions/InstructionsGetSourcesResult" }, + }, + reload: { + rpcMethod: "session.instructions.reload", + params: null, + result: { $ref: "#/definitions/InstructionsGetSourcesResult" }, + }, + }, + }, + }; + const apiSchema = schema as ApiSchema; + + const types = generateApiTypesCode(apiSchema); + const rpc = generateRpcCode(apiSchema); + expect(types).toContain("pub struct InstructionsGetSourcesResult {"); + expect(types).not.toContain("pub struct SessionInstructionsReloadResult {"); + expect(rpc).toContain( + "pub async fn reload(&self) -> Result" + ); + + schema.session.instructions.reload.result.$ref = "#/definitions/OtherResult"; + expect(generateApiTypesCode(apiSchema)).toContain( + "pub struct SessionInstructionsReloadResult {" + ); + expect(generateRpcCode(apiSchema)).toContain( + "pub async fn reload(&self) -> Result" + ); + }); + it.each([true, false])( "validates a reference's sibling constant without changing its enum (required: %s)", (required) => { @@ -604,6 +658,49 @@ pub enum ${name} { expect(empty).not.toContain("pub type SessionSearchData"); }); + it("types the durable human response event union", () => { + const response: JSONSchema7 = { + title: "HumanResponseRecordedResponse", + anyOf: [ + { + type: "object", + required: ["responseKind", "answer"], + properties: { + responseKind: { type: "string", const: "user_input" }, + answer: { type: "string" }, + }, + }, + { + type: "object", + required: ["responseKind", "approved"], + properties: { + responseKind: { type: "string", const: "exit_plan_mode" }, + approved: { type: "boolean" }, + }, + }, + ], + }; + const code = generateSessionEventsCode( + eventSchema( + { + type: "object", + required: ["response"], + properties: { + response: { $ref: "#/definitions/HumanResponseRecordedResponse" }, + }, + }, + { HumanResponseRecordedResponse: response } + ) + ); + + expect(code).toContain("pub response: HumanResponseRecordedResponse,"); + expect(code).toContain(`pub enum HumanResponseRecordedResponse { + UserInput(HumanResponseRecordedResponseUserInput), + ExitPlanMode(HumanResponseRecordedResponseExitPlanMode), +}`); + expect(code).not.toContain("pub response: serde_json::Value,"); + }); + it.each(["reasonCode", "judgeStatus", "evaluationStage"])( "preserves explicit unknown values in nested approval %s enums", (property) => { diff --git a/nodejs/test/session-event-codegen.test.ts b/nodejs/test/session-event-codegen.test.ts index 60906720de..c18f190bda 100644 --- a/nodejs/test/session-event-codegen.test.ts +++ b/nodejs/test/session-event-codegen.test.ts @@ -2,7 +2,11 @@ import type { JSONSchema7 } from "json-schema"; import { describe, expect, it } from "vitest"; import { generateSessionEventsCode as generateCSharpSessionEventsCode } from "../../scripts/codegen/csharp.ts"; -import { generateGoSessionEventsCode } from "../../scripts/codegen/go.ts"; +import { + collectGoDeprecatedEventNames, + generateGoSessionEventAliasFile, + generateGoSessionEventsCode, +} from "../../scripts/codegen/go.ts"; import { generatePythonSessionEventsCode, postProcessExternalRefsForPython, @@ -10,6 +14,67 @@ import { import { generateSessionEventsCode as generateRustSessionEventsCode } from "../../scripts/codegen/rust.ts"; describe("session event codegen", () => { + it.each([ + ["inline", "legacy"], + ["reference", "legacy"], + ["inline", "someEvent"], + ["reference", "someEvent"], + ])("deprecates the whole %s %s event across SDK projections", (shape, eventName) => { + const event = { + type: "object", + deprecated: true, + required: ["type", "data"], + properties: { + type: { const: `tool.${eventName}` }, + data: { type: "object", properties: { text: { type: "string" } } }, + }, + } satisfies JSONSchema7 & { deprecated: boolean }; + const schema: JSONSchema7 = { + definitions: { + LegacyEvent: event, + SessionEvent: { + anyOf: [shape === "inline" ? event : { $ref: "#/definitions/LegacyEvent" }], + }, + }, + }; + + const className = `Tool${eventName[0].toUpperCase()}${eventName.slice(1)}`; + expect(generateCSharpSessionEventsCode(schema)).toMatch( + new RegExp( + `\\[Obsolete\\([^\\n]+\\)\\]\\n#endif\\npublic sealed partial class ${className}Event` + ) + ); + const { typeCode } = generateGoSessionEventsCode(schema, "rpc"); + expect(typeCode).toMatch( + new RegExp(`// Deprecated:[^\\n]+\\n\\s*SessionEventType${className} `) + ); + expect(typeCode).toMatch( + new RegExp(`//\\n// Deprecated:[^\\n]+\\ntype ${className}Data struct`) + ); + const aliases = generateGoSessionEventAliasFile( + typeCode, + [], + [], + [], + collectGoDeprecatedEventNames(schema) + ); + expect(aliases).toMatch( + new RegExp(`// Deprecated:[^\\n]+\\n\\s*${className}Data = rpc\\.${className}Data`) + ); + expect(aliases).toMatch( + new RegExp(`// Deprecated:[^\\n]+\\n\\s*SessionEventType${className} = rpc\\.`) + ); + const rust = generateRustSessionEventsCode(schema); + expect( + rust.match( + new RegExp( + `#\\[deprecated\\]\\n\\s*#\\[serde\\(rename = "tool\\.${eventName}"\\)\\]`, + "g" + ) + ) + ).toHaveLength(2); + }); + describe.each(["anyOf", "oneOf"] as const)("Go root %s payloads", (unionKeyword) => { it.each(["inline", "local", "shared"])( "preserves %s union payloads as raw JSON", diff --git a/nodejs/test/session-event-types.test.ts b/nodejs/test/session-event-types.test.ts index 330b381090..de03f98142 100644 --- a/nodejs/test/session-event-types.test.ts +++ b/nodejs/test/session-event-types.test.ts @@ -44,6 +44,8 @@ import type { ResumeData, StartData, ToolExecutionCompleteData, + ToolExecutionCompleteFileEdit, + ToolExecutionCompleteFileEditKind, ToolExecutionPartialData, ToolExecutionProgressData, ToolExecutionStartData, @@ -220,6 +222,21 @@ describe("Session event type exports (#1156)", () => { expect(data.turnId).toBe("turn-1"); }); + it.each([ + "create", + "edit", + "delete", + "future_kind", + ] satisfies ToolExecutionCompleteFileEditKind[])( + "preserves file edit kind %s in completion data", + (kind) => { + const edit: ToolExecutionCompleteFileEdit = { path: "/session/file.txt", kind }; + const data: Pick = { fileEdits: [edit] }; + + expect(JSON.parse(JSON.stringify(data))).toEqual({ fileEdits: [edit] }); + } + ); + it("exposes explicit user approval metadata for managed Domain requests", () => { const request: PermissionRequest = { kind: "url", diff --git a/nodejs/test/session_fs_adapter.test.ts b/nodejs/test/session_fs_adapter.test.ts index 98749dffba..49591f044c 100644 --- a/nodejs/test/session_fs_adapter.test.ts +++ b/nodejs/test/session_fs_adapter.test.ts @@ -4,7 +4,11 @@ import { MemoryProvider } from "@platformatic/vfs"; import { describe, expect, it } from "vitest"; -import { createSessionFsAdapter, type SessionFsProvider } from "../src/index.js"; +import { + createSessionFsAdapter, + SessionFsWriteFailure, + type SessionFsProvider, +} from "../src/index.js"; describe("SessionFsAdapter", () => { it("should map all sessionFs handler operations", async () => { @@ -16,6 +20,12 @@ describe("SessionFsAdapter", () => { async readFile(path) { return (await memoryProvider.readFile(sp(path), "utf8")) as string; }, + async readFileBytes(path) { + return memoryProvider.readFile(sp(path)); + }, + async writeFileBytes(path, content) { + await memoryProvider.writeFile(sp(path), content); + }, async writeFile(path, content) { await memoryProvider.writeFile(sp(path), content); }, @@ -126,6 +136,76 @@ describe("SessionFsAdapter", () => { expect(content.content).toBe("hello world"); expect(content.error).toBeUndefined(); + await memoryProvider.writeFile( + sp("/workspace/nested/photo.png"), + Buffer.from([0, 255, 254, 1]) + ); + const binary = await handler.readFileBytes({ + sessionId, + path: "/workspace/nested/photo.png", + }); + expect(binary.content).toBe("AP/+AQ=="); + expect(binary.error).toBeUndefined(); + const written = await handler.writeFileBytes({ + sessionId, + path: "/workspace/nested/written.bin", + content: "AP/+AQ==", + mode: 0o600, + }); + expect(written).toBeUndefined(); + expect(await memoryProvider.readFile(sp("/workspace/nested/written.bin"))).toEqual( + Buffer.from([0, 255, 254, 1]) + ); + expect( + await handler.writeFileBytes({ + sessionId, + path: "/workspace/nested/invalid.bin", + content: "AA==AAAA", + }) + ).toMatchObject({ + code: "UNKNOWN", + message: "invalid sessionFs.writeFileBytes base64 content", + }); + expect(await memoryProvider.exists(sp("/workspace/nested/invalid.bin"))).toBe(false); + expect( + await handler.writeFileBytes({ + sessionId, + path: "/workspace/nested/large.bin", + content: "A".repeat(64 * 1024 * 1024), + }) + ).toMatchObject({ + code: "UNKNOWN", + message: "sessionFs.writeFileBytes content exceeds the binary write limit", + }); + expect(await memoryProvider.exists(sp("/workspace/nested/large.bin"))).toBe(false); + const missingBinary = await createSessionFsAdapter({ + ...provider, + readFileBytes: undefined, + }).readFileBytes({ + sessionId, + path: "/workspace/nested/photo.png", + }); + expect(missingBinary.error?.code).toBe("UNKNOWN"); + expect( + await createSessionFsAdapter({ ...provider, writeFileBytes: undefined }).writeFileBytes( + { + sessionId, + path: "/workspace/nested/missing.bin", + content: "AP/+AQ==", + } + ) + ).toMatchObject({ code: "UNKNOWN" }); + + const oversized = await createSessionFsAdapter({ + ...provider, + readFileBytes: async () => new Uint8Array(((64 * 1024 * 1024 - 1024) / 4) * 3 + 1), + }).readFileBytes({ sessionId, path: "/workspace/nested/photo.png" }); + expect(oversized.content).toBe(""); + expect(oversized.error).toEqual({ + code: "UNKNOWN", + message: "sessionFs.readFileBytes content exceeds the binary read limit", + }); + const entries = await handler.readdir({ sessionId, path: "/workspace/nested" }); expect(entries.entries).toContain("file.txt"); expect(entries.error).toBeUndefined(); @@ -275,5 +355,22 @@ describe("SessionFsAdapter", () => { }); expect(unknownError).toBeDefined(); expect(unknownError!.code).toBe("UNKNOWN"); + + const changedProvider = createSessionFsAdapter( + makeThrowingProvider(new SessionFsWriteFailure("write truncated the target")) + ); + expect( + await changedProvider.writeFile({ sessionId, path: "changed.txt", content: "content" }) + ).toEqual({ + code: "UNKNOWN", + message: "write truncated the target", + writeChanged: true, + }); + expect( + await changedProvider.appendFile({ sessionId, path: "changed.txt", content: "content" }) + ).not.toHaveProperty("writeChanged"); + expect( + await handler.writeFile({ sessionId, path: "missing.txt", content: "content" }) + ).not.toHaveProperty("writeChanged"); }); }); diff --git a/nodejs/test/shared-codegen.test.ts b/nodejs/test/shared-codegen.test.ts index 1980978936..91e29d07d0 100644 --- a/nodejs/test/shared-codegen.test.ts +++ b/nodejs/test/shared-codegen.test.ts @@ -13,11 +13,44 @@ import { findSharedSchemaDefinitions, getEnumValueDescriptions, inlineExternalSchemaDefinitions, + isCodegenEntrypoint, isIntegerSchemaBoundedToInt32, rewriteSharedDefinitionReferences, resolveCopilotSchemaPaths, } from "../../scripts/codegen/utils.ts"; +describe("codegen entrypoints", () => { + it("recognizes Windows entrypoint paths case-insensitively", () => { + expect( + isCodegenEntrypoint("C:\\b\\execroot\\python.ts", "c:\\B\\execroot\\python.ts", "win32") + ).toBe(true); + }); + + it("rejects other modules and missing entrypoints", () => { + expect(isCodegenEntrypoint(undefined, "python.ts")).toBe(false); + expect(isCodegenEntrypoint("go.ts", "python.ts")).toBe(false); + expect(isCodegenEntrypoint("PYTHON.ts", "python.ts", "linux")).toBe(false); + }); + + it("recognizes a Bazel-style directory symlink to the generator", async () => { + const root = await mkdtemp(join(tmpdir(), "copilot-codegen-entrypoint-")); + onTestFinished(() => rm(root, { recursive: true, force: true })); + const source = join(root, "source"); + const execroot = join(root, "execroot"); + await mkdir(source); + await writeFile(join(source, "python.ts"), ""); + await writeFile(join(source, "other.ts"), ""); + await symlink(source, execroot, process.platform === "win32" ? "junction" : "dir"); + expect(isCodegenEntrypoint(join(execroot, "python.ts"), join(source, "python.ts"))).toBe( + true + ); + expect(isCodegenEntrypoint(join(execroot, "other.ts"), join(source, "python.ts"))).toBe( + false + ); + expect(isCodegenEntrypoint("", join(source, "python.ts"))).toBe(false); + }); +}); + describe("shared schema definition codegen utilities", () => { it.each(["typescript", "python", "go", "csharp"])( "runs the %s generator through a linked entrypoint", diff --git a/nodejs/test/testBackend.test.ts b/nodejs/test/testBackend.test.ts new file mode 100644 index 0000000000..94f74aaccf --- /dev/null +++ b/nodejs/test/testBackend.test.ts @@ -0,0 +1,74 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +import { describe, expect, it } from "vitest"; +import type { ResumeSessionConfig, SessionConfig } from "../src"; +import { parseTestBackend, withTestBackend } from "./e2e/harness/testBackend"; + +describe("E2E backend selection", () => { + it.each([ + [undefined, "capi"], + ["", "capi"], + ["capi", "capi"], + [" ANTHROPIC-MESSAGES ", "anthropic-messages"], + ["openai-responses", "openai-responses"], + ["openai-completions", "openai-completions"], + ])("parses %s as %s", (value, expected) => { + expect(parseTestBackend(value)).toBe(expected); + }); + + it("rejects an unknown backend rather than running CAPI", () => { + expect(() => parseTestBackend("openai-response")).toThrow( + "Unsupported COPILOT_SDK_E2E_BACKEND" + ); + }); + + it.each([ + ["anthropic-messages", "anthropic", undefined, "claude-sonnet-5"], + ["openai-responses", "openai", "responses", "gpt-4.1"], + ["openai-completions", "openai", "completions", "gpt-4.1"], + ] as const)("configures %s for create and resume", (backend, type, wireApi, model) => { + const create: SessionConfig = { sessionId: "new-session" }; + const resume: ResumeSessionConfig = { disableResume: true }; + for (const config of [create, resume]) { + expect(withTestBackend(config, backend, "http://localhost:1234")).toEqual({ + ...config, + model, + provider: { + baseUrl: "http://localhost:1234", + type, + wireApi, + bearerToken: "fake-byok-credential-for-e2e-tests", + modelId: model, + wireModel: model, + }, + }); + expect(config).not.toHaveProperty("provider"); + } + }); + + it("preserves explicit models and provider configurations", () => { + const explicitModel: SessionConfig = { model: "test-model" }; + expect( + withTestBackend(explicitModel, "openai-responses", "http://localhost:1234") + ).toMatchObject({ + model: "test-model", + provider: { modelId: "test-model", wireModel: "test-model" }, + }); + const configs: SessionConfig[] = [ + { provider: { type: "openai", baseUrl: "https://example.com" } }, + { providers: [] }, + ]; + for (const config of configs) { + expect(withTestBackend(config, "openai-responses", "http://localhost:1234")).toBe( + config + ); + } + }); + + it("leaves CAPI configurations unchanged", () => { + const config: SessionConfig = { model: "test-model" }; + expect(withTestBackend(config, "capi", "http://localhost:1234")).toBe(config); + }); +}); diff --git a/nodejs/test/typescript-codegen.test.ts b/nodejs/test/typescript-codegen.test.ts index 181cb6b781..1ff222dd19 100644 --- a/nodejs/test/typescript-codegen.test.ts +++ b/nodejs/test/typescript-codegen.test.ts @@ -1,6 +1,6 @@ import type { JSONSchema7 } from "json-schema"; import { compile } from "json-schema-to-typescript"; -import { describe, expect, it } from "vitest"; +import { describe, expect, expectTypeOf, it } from "vitest"; import { assertNoPublicInternalReferences, @@ -8,6 +8,7 @@ import { normalizeSchemaForTypeScript, tsNullableResultTypeName, } from "../../scripts/codegen/typescript.ts"; +import type { AssistantMessageReasoningBlocks } from "../src/generated/session-events.ts"; import { isCodegenEntrypoint, type DefinitionCollections } from "../../scripts/codegen/utils.ts"; describe("typescript schema codegen", () => { @@ -39,6 +40,14 @@ describe("typescript schema codegen", () => { ).toBe("SessionAccount | undefined"); }); + it("keeps reasoning-block SDK types permissive like the other language projections", () => { + expectTypeOf<{ + provider: string; + blocks: []; + orderedBlocks: []; + }>().toExtend(); + }); + it("recognizes Windows entrypoint paths case-insensitively", () => { expect( isCodegenEntrypoint( @@ -88,6 +97,40 @@ describe("typescript schema codegen", () => { expect(code).toContain('inlineMode: /** Use a direct value. */ "direct" | "indirect";'); }); + it("accepts future reload values without widening other enum definitions", async () => { + const enumDefinition = (value: string): JSONSchema7 => ({ + type: "string", + enum: [value], + "x-enumDescriptions": { [value]: `Known ${value} value.` }, + }); + const code = await compile( + normalizeSchemaForTypeScript({ + title: "ReloadOptions", + type: "object", + properties: { + status: { $ref: "#/definitions/CustomizationReloadStatus" }, + subsystem: { $ref: "#/definitions/CustomizationReloadSubsystem" }, + other: { $ref: "#/definitions/OtherStatus" }, + }, + definitions: { + CustomizationReloadStatus: enumDefinition("reloaded"), + CustomizationReloadSubsystem: enumDefinition("skills"), + OtherStatus: enumDefinition("known"), + }, + }), + "ReloadOptions", + { bannerComment: "", style: { semi: true, singleQuote: false } } + ); + + expect(code).toMatch( + /export type CustomizationReloadStatus\s*=\s*\/\*\* Known reloaded value\. \*\/ "reloaded" \| \(string & \{\}\);/ + ); + expect(code).toMatch( + /export type CustomizationReloadSubsystem\s*=\s*\/\*\* Known skills value\. \*\/ "skills" \| \(string & \{\}\);/ + ); + expect(code).toContain('export type OtherStatus = /** Known known value. */ "known";'); + }); + it("maps bare opaque properties to their marker aliases", async () => { const code = await compile( normalizeSchemaForTypeScript({ diff --git a/nodejs/vitest.config.ts b/nodejs/vitest.config.ts index f5dca5f7a3..10a9ac4515 100644 --- a/nodejs/vitest.config.ts +++ b/nodejs/vitest.config.ts @@ -1,4 +1,5 @@ import { defineConfig } from "vitest/config"; +import { isByokBackend } from "./test/e2e/harness/testBackend"; const integrationTestTimeout = process.platform === "win32" ? 60000 : 30000; @@ -25,6 +26,7 @@ const inProcessBlockedE2E = [ "**/test/e2e/hooks.e2e.test.ts", "**/test/e2e/hooks_extended.e2e.test.ts", "**/test/e2e/mcp_and_agents.e2e.test.ts", + "**/test/e2e/mcp_instruction_recovery.e2e.test.ts", "**/test/e2e/memory_retrieval.e2e.test.ts", "**/test/e2e/mode_empty.e2e.test.ts", "**/test/e2e/multi_turn.e2e.test.ts", @@ -52,8 +54,26 @@ const inProcessBlockedE2E = [ "**/test/e2e/tools.e2e.test.ts", ]; +// These suites own their provider or CAPI auth/request-handler setup. They run +// in the ordinary CAPI job, not as repetitions of the matrix-selected backend. +const selfConfiguredBackendE2E = [ + "**/test/e2e/byok_bearer_token_provider.e2e.test.ts", + "**/test/e2e/copilot_request_handler.e2e.test.ts", + "**/test/e2e/copilot_request_session_id.e2e.test.ts", + "**/test/e2e/disabled_mcp_servers.e2e.test.ts", + "**/test/e2e/mcp_cached_oauth_overlap.e2e.test.ts", + "**/test/e2e/mcp_catalog_recovery.e2e.test.ts", + "**/test/e2e/memory_retrieval.e2e.test.ts", + "**/test/e2e/mode_handlers.e2e.test.ts", + "**/test/e2e/multi_provider_registry.e2e.test.ts", + "**/test/e2e/per_session_auth.e2e.test.ts", + "**/test/e2e/provider_endpoint.e2e.test.ts", + "**/test/e2e/subagent_prompt_resume.e2e.test.ts", +]; + export default defineConfig({ test: { + ...(isByokBackend ? { include: ["test/e2e/*.e2e.test.ts"] } : {}), globals: true, environment: "node", testTimeout: integrationTestTimeout, @@ -69,6 +89,7 @@ export default defineConfig({ "**/*.d.ts", "**/basic-test.ts", // Old manual test ...(isInProcessTransport ? inProcessBlockedE2E : []), + ...(isByokBackend ? selfConfiguredBackendE2E : []), ], }, }); diff --git a/package.json b/package.json index d7ad4de360..34d2bde533 100644 --- a/package.json +++ b/package.json @@ -3,6 +3,7 @@ "private": true, "scripts": { "build": "node scripts/run-tasks.mjs build", + "build:default": "node scripts/run-tasks.mjs build:default", "build:nodejs": "node scripts/run-tasks.mjs build nodejs", "build:python": "node scripts/run-tasks.mjs build python", "build:go": "node scripts/run-tasks.mjs build go", diff --git a/python/README.md b/python/README.md index d2ebf5fc2e..04cefd38f4 100644 --- a/python/README.md +++ b/python/README.md @@ -259,7 +259,7 @@ All options are kw-only parameters: - `installation_confirmation_handler` (InstallationConfirmationHandler | None): Experimental connection-global human review for `installations.confirm`. Receives the typed request and one cancellation signal, and returns an explicit decision. Does not enable installation capabilities. - `use_logged_in_user` (bool | None): Whether to use logged-in user for authentication (default: True, but False when `github_token` is provided). - `telemetry` (dict | None): OpenTelemetry configuration for the CLI process. Providing this enables telemetry — no separate flag needed. See [Telemetry](#telemetry) below. -- `session_fs` (dict | None): Connection-level session filesystem provider configuration. +- `session_fs` (dict | None): Connection-level session filesystem provider configuration. To let `view` read provider-only images, set `capabilities.binary` to `True` and return a provider implementing `SessionFsBinaryProvider` (`read_file_bytes(path)` and `write_file_bytes(path, content, mode=None)`) from `create_session_fs_handler`; image reads do not fall back to local files. Binary reads and writes are limited to 50,330,880 raw bytes (approximately 48 MiB); larger results return a filesystem error before encoding or decoding. - `session_idle_timeout_seconds` (int | None): Server-wide session idle timeout in seconds. Set to `None` or `0` to disable. - `enable_remote_sessions` (bool): Enable remote/cloud session support (default: False). - `on_list_models` (callable | None): Custom handler for `list_models()`. When provided, the handler is called instead of querying the runtime. @@ -553,6 +553,30 @@ async def edit_file(params: EditFileParams) -> str: # your logic ``` +An explicit `apply_patch` override can declare a root string schema with +`RootModel[str]`. The model sees a required `input` property, but the runtime +restores the scalar patch text before dispatch. Access it through `params.root`; +`ToolInvocation.arguments` also contains the string, not an `{"input": ...}` +object. This example returns trimmed patch text; replace the handler body with +your own patch implementation: + +```python +from pydantic import RootModel +from copilot import define_tool + + +@define_tool( + "apply_patch", + description="Apply a patch", + overrides_built_in_tool=True, +) +def apply_patch(params: RootModel[str]) -> str: + return params.root.strip() +``` + +String-schema `apply_patch` overrides cannot contain JSON Schema references; +use an object schema if references are needed. + #### Skipping Permission Prompts Set `skip_permission=True` on a tool definition to allow it to execute without triggering a permission prompt: @@ -912,6 +936,8 @@ async with await client.create_session( Available section IDs: `"preamble"`, `"identity"`, `"tone"`, `"tool_efficiency"`, `"environment_context"`, `"code_change_rules"`, `"guidelines"`, `"safety"`, `"tool_instructions"`, `"custom_instructions"`, `"runtime_instructions"`, `"last_instructions"`. `"identity"` and `"tool_instructions"` are section groups that target a collection of related sub-sections as a unit; use `"preamble"` to target just the identity preamble. +`last_instructions` includes configured subagent-model guidance when the `task` tool is available. Removing or replacing this section also removes that guidance; a transform callback receives the complete section, including the guidance, and its returned content is authoritative. Append, prepend, and preserve retain their usual section semantics. These overrides change prompt prose only, not configured subagent models, tool availability, or runtime dispatch policy. `runtime_instructions` is a separate section: removing it does not remove `last_instructions`. + Each section override supports five string actions: `"replace"`, `"remove"`, `"append"`, `"prepend"`, and `"preserve"` (a no-op that opts an individually-addressable section out of a group-level `"remove"`). Unknown section IDs are handled gracefully: content from `"replace"`/`"append"`/`"prepend"` overrides is appended to additional instructions, and `"remove"` overrides are silently ignored. You can also pass a transform callback as the `action` instead of a string. The callback receives the current section content and returns the new content (sync or async): @@ -1125,6 +1151,14 @@ async with await client.create_session( Hook into session lifecycle events by providing handlers in the `hooks` configuration: ```python +from copilot import ( + SubagentStartHookInput, + SubagentStartHookOutput, + SubagentStopHookInput, + SubagentStopHookOutput, +) + + async def on_pre_tool_use(input, invocation): print(f"About to run tool: {input['toolName']}") # Return permission decision and optionally modify args @@ -1170,6 +1204,22 @@ async def on_session_end(input, invocation): print(f"Session ended: {input['reason']}") +async def on_subagent_start( + input: SubagentStartHookInput, invocation: dict[str, str] +) -> SubagentStartHookOutput: + print(f"Starting {input['agentName']}") + return {"additionalContext": "Report the complete result of your work."} + + +async def on_subagent_stop( + input: SubagentStopHookInput, invocation: dict[str, str] +) -> SubagentStopHookOutput: + print(f"{input['agentName']} finished: {input['stopReason']}") + # To ask for another subagent turn instead, return + # {"decision": "block", "reason": "Finish the remaining work"}. + return {"modifiedResponse": f"Verified result: {input['response']}"} + + async def on_error_occurred(input, invocation): print(f"Error in {input['errorContext']}: {input['error']}") return { @@ -1187,6 +1237,8 @@ async with await client.create_session( "on_user_prompt_submitted": on_user_prompt_submitted, "on_session_start": on_session_start, "on_session_end": on_session_end, + "on_subagent_start": on_subagent_start, + "on_subagent_stop": on_subagent_stop, "on_error_occurred": on_error_occurred, }, ) as session: @@ -1201,8 +1253,21 @@ async with await client.create_session( - `on_user_prompt_submitted` - Intercept user prompts. Can modify the prompt before processing. - `on_session_start` - Run logic when a session starts or resumes. - `on_session_end` - Cleanup or logging when session ends. +- `on_subagent_start` - Inspect a subagent launch and optionally prepend `additionalContext` to its prompt. +- `on_subagent_stop` - Inspect a completed subagent turn and optionally replace its `response` with `modifiedResponse` (with or without `"decision": "allow"`), or return `{"decision": "block", "reason": "..."}` to ask it to continue. - `on_error_occurred` - Handle errors with retry/skip/abort strategies. +Subagent hook inputs carry the **parent** `sessionId`, a timezone-aware `datetime` +`timestamp`, `workingDirectory` (normalized from the runtime's `cwd`), +`transcriptPath`, and `agentName`. The start hook may include +`agentDisplayName` and `agentDescription`. The stop hook also carries +`agentType`, `stopReason` (`"end_turn"`), the subagent's final `response`, and +optional `agentId`, `agentDisplayName`, and `agentDescription`. Handlers may +be synchronous or asynchronous and may return `None`. For typed handlers, +import `SubagentStartHookInput`, `SubagentStartHookOutput`, +`SubagentStartHandler`, `SubagentStopHookInput`, `SubagentStopHookOutput`, +and `SubagentStopHandler` from `copilot`. + ## Commands Register slash commands that users can invoke from the CLI TUI. When the user types `/commandName`, the SDK dispatches the event to your handler. diff --git a/python/copilot/__init__.py b/python/copilot/__init__.py index d2295b4745..bb26e4b9c5 100644 --- a/python/copilot/__init__.py +++ b/python/copilot/__init__.py @@ -209,6 +209,12 @@ SessionStartHookOutput, SessionUiApi, SessionUiCapabilities, + SubagentStartHandler, + SubagentStartHookInput, + SubagentStartHookOutput, + SubagentStopHandler, + SubagentStopHookInput, + SubagentStopHookOutput, SystemMessageConfig, ToolSearchConfig, TranscriptRecoveryReport, @@ -224,11 +230,13 @@ create_attributed_permission_result, ) from .session_fs_provider import ( + SessionFsBinaryProvider, SessionFsFileInfo, SessionFsProvider, SessionFsSqliteProvider, SessionFsSqliteQueryResult, SessionFsSqliteTransactionFailure, + SessionFsWriteFailure, create_session_fs_adapter, ) from .tools import ( @@ -415,9 +423,11 @@ "SessionFsConfig", "SessionFsFileInfo", "SessionFsProvider", + "SessionFsBinaryProvider", "SessionFsSqliteProvider", "SessionFsSqliteQueryResult", "SessionFsSqliteTransactionFailure", + "SessionFsWriteFailure", "SessionHooks", "SessionLimitsConfig", "SessionLifecycleEvent", @@ -435,6 +445,12 @@ "SessionUpdatedEvent", "StdioRuntimeConnection", "StopError", + "SubagentStartHandler", + "SubagentStartHookInput", + "SubagentStartHookOutput", + "SubagentStopHandler", + "SubagentStopHookInput", + "SubagentStopHookOutput", "SystemMessageConfig", "TcpRuntimeConnection", "TelemetryConfig", diff --git a/python/copilot/_cli_download.py b/python/copilot/_cli_download.py index 72424cb650..d36b6ff5ec 100644 --- a/python/copilot/_cli_download.py +++ b/python/copilot/_cli_download.py @@ -252,6 +252,7 @@ def _fetch_url_bytes(url: str, *, timeout: int) -> bytes: "app.js", "assets", "changelog.json", + "cli-main.js", "copilot", "copilot.exe", "copilot-sdk", diff --git a/python/copilot/canvas.py b/python/copilot/canvas.py index 9b8dec5258..9beab562d7 100644 --- a/python/copilot/canvas.py +++ b/python/copilot/canvas.py @@ -188,5 +188,12 @@ async def on_close(self, ctx: CanvasProviderCloseRequest) -> None: """Canvas was closed by the user or agent. Default: no-op.""" async def on_action(self, ctx: CanvasProviderInvokeActionRequest) -> Any: - """Handle a non-lifecycle action declared by the canvas.""" + """Handle a non-lifecycle action declared by the canvas. + + The returned value is sent to the model as the ``invoke_canvas_action`` + tool result. To return text and images, as tool handlers do, return a + tool-result dict in wire form (``textResultForLlm``, ``resultType`` and + ``binaryResultsForLlm``); any other value is rendered to the model as + JSON text. + """ raise CanvasError.no_handler() diff --git a/python/copilot/client.py b/python/copilot/client.py index 20b98b4878..5f6f69cb4f 100644 --- a/python/copilot/client.py +++ b/python/copilot/client.py @@ -2958,6 +2958,14 @@ def _initialize_session(sid: str) -> CopilotSession: "SessionFs capabilities declare SQLite support but the provider " "does not implement SessionFsSqliteProvider" ) + if caps and caps.get("binary"): + from .session_fs_provider import SessionFsBinaryProvider + + if not isinstance(fs_provider, SessionFsBinaryProvider): + raise ValueError( + "SessionFs capabilities declare binary support but the provider " + "does not implement SessionFsBinaryProvider" + ) s._client_session_apis.session_fs = create_session_fs_adapter(fs_provider) s._register_tools(tools) s._register_commands(commands) @@ -2996,6 +3004,20 @@ def _initialize_session(sid: str) -> CopilotSession: session: CopilotSession | None = None registered_session_id: str | None = None + server_assigned_session_id: str | None = None + inline_state_lock = threading.Lock() + request_failed = False + event_loop = asyncio.get_running_loop() + + async def _delete_uninitialized_session(sid: str) -> None: + try: + await asyncio.wait_for(self.delete_session(sid), timeout=10) + except Exception: + logger.warning( + "Failed to delete cloud session %s after creation failed", + sid, + exc_info=True, + ) # Pre-register non-cloud sessions BEFORE issuing the RPC so any # session-scoped requests the CLI emits during session.create @@ -3020,15 +3042,40 @@ def _initialize_session(sid: str) -> CopilotSession: # would silently drop because the session id isn't yet # registered. Non-cloud sessions are already registered above. def _register_inline(raw_response: Any) -> None: - nonlocal session, registered_session_id + nonlocal session, registered_session_id, server_assigned_session_id if session is not None: return if not isinstance(raw_response, dict): return sid = raw_response.get("sessionId") if isinstance(sid, str) and sid: - session = _initialize_session(sid) - registered_session_id = sid + try: + initialized = _initialize_session(sid) + except Exception: + with inline_state_lock: + late_failure = request_failed + if not late_failure: + server_assigned_session_id = sid + if late_failure: + cleanup = _delete_uninitialized_session(sid) + try: + asyncio.run_coroutine_threadsafe(cleanup, event_loop) + except RuntimeError: + cleanup.close() + logger.warning( + "Failed to schedule cleanup of cloud session %s", + sid, + exc_info=True, + ) + raise + with inline_state_lock: + late_success = request_failed + if not late_success: + session = initialized + registered_session_id = sid + if late_success: + with self._sessions_lock: + self._sessions.pop(sid, None) response = await self._client.request( "session.create", payload, on_response_inline=_register_inline @@ -3059,9 +3106,15 @@ def _register_inline(raw_response: Any) -> None: capabilities = response.get("capabilities") session._set_capabilities(capabilities) except BaseException as exc: - if registered_session_id is not None: + with inline_state_lock: + request_failed = True + registered_id = registered_session_id + orphaned_id = server_assigned_session_id + if registered_id is not None: with self._sessions_lock: - self._sessions.pop(registered_session_id, None) + self._sessions.pop(registered_id, None) + if orphaned_id is not None: + await _delete_uninitialized_session(orphaned_id) self._unregister_github_token_provider(github_token_provider_registration_id) if not isinstance(exc, asyncio.CancelledError): log_timing( @@ -3676,6 +3729,14 @@ async def resume_session( "SessionFs capabilities declare SQLite support but the provider " "does not implement SessionFsSqliteProvider" ) + if caps and caps.get("binary"): + from .session_fs_provider import SessionFsBinaryProvider + + if not isinstance(fs_provider, SessionFsBinaryProvider): + raise ValueError( + "SessionFs capabilities declare binary support but the provider " + "does not implement SessionFsBinaryProvider" + ) session._client_session_apis.session_fs = create_session_fs_adapter(fs_provider) session._register_tools(tools) session._register_commands(commands) diff --git a/python/copilot/generated/rpc.py b/python/copilot/generated/rpc.py index 4e7d7e319c..e30e815dea 100644 --- a/python/copilot/generated/rpc.py +++ b/python/copilot/generated/rpc.py @@ -2903,6 +2903,11 @@ class CurrentModel: """Latest unclaimed Auto preference waiting for a future user turn. Null means the pending request is returning to provider-default routing. """ + plan_base_model_id: str | None = None + """Captured base model to restore when leaving plan mode. Omitted outside plan mode or when + no plan override has captured a base model. Persistent agent model requirements apply to + this model rather than the temporary plan model. + """ reasoning_effort: str | None = None """Reasoning effort level currently applied to the active model, when one is set. Reads `Session.getReasoningEffort()` synchronously after `getSelectedModel()` resolves so the @@ -2917,8 +2922,9 @@ def from_dict(obj: Any) -> 'CurrentModel': context_tier = from_union([ContextTier, from_none], obj.get("contextTier")) model_id = from_union([from_str, from_none], obj.get("modelId")) pending_auto_tier = from_union([AutoTier, from_none], obj.get("pendingAutoTier")) + plan_base_model_id = from_union([from_str, from_none], obj.get("planBaseModelId")) reasoning_effort = from_union([from_str, from_none], obj.get("reasoningEffort")) - return CurrentModel(activating_auto_tier, auto_tier, context_tier, model_id, pending_auto_tier, reasoning_effort) + return CurrentModel(activating_auto_tier, auto_tier, context_tier, model_id, pending_auto_tier, plan_base_model_id, reasoning_effort) def to_dict(self) -> dict: result: dict = {} @@ -2932,10 +2938,55 @@ def to_dict(self) -> dict: result["modelId"] = from_union([from_str, from_none], self.model_id) if self.pending_auto_tier is not None: result["pendingAutoTier"] = from_union([lambda x: to_enum(AutoTier, x), from_none], self.pending_auto_tier) + if self.plan_base_model_id is not None: + result["planBaseModelId"] = from_union([from_str, from_none], self.plan_base_model_id) if self.reasoning_effort is not None: result["reasoningEffort"] = from_union([from_str, from_none], self.reasoning_effort) return result +# Experimental: this type is part of an experimental API and may change or be removed. +class CustomizationReloadStatus(Enum): + """Whether the component reloaded, was skipped, or failed + + Result of reloading a customization component. + """ + FAILED = "failed" + RELOADED = "reloaded" + SKIPPED = "skipped" + + @classmethod + def _missing_(cls, value: object) -> CustomizationReloadStatus | None: + if not isinstance(value, str): + return None + member = object.__new__(cls) + member._name_ = "UNKNOWN" + member._value_ = value + return cls._value2member_map_.setdefault(value, member) + +# Experimental: this type is part of an experimental API and may change or be removed. +class CustomizationReloadSubsystem(Enum): + """Component whose reload was attempted or skipped + + Component of session customization discovery. + """ + AGENTS = "agents" + EXTENSIONS = "extensions" + HOOKS = "hooks" + INSTRUCTIONS = "instructions" + MCP = "mcp" + PLUGINS = "plugins" + REPOSITORY_CONTEXT = "repositoryContext" + SKILLS = "skills" + + @classmethod + def _missing_(cls, value: object) -> CustomizationReloadSubsystem | None: + if not isinstance(value, str): + return None + member = object.__new__(cls) + member._name_ = "UNKNOWN" + member._value_ = value + return cls._value2member_map_.setdefault(value, member) + # Experimental: this type is part of an experimental API and may change or be removed. class DebugCollectLogsSource(Enum): """Source category for this entry. @@ -3261,6 +3312,142 @@ class DiscoveredMCPServerType(Enum): SSE = "sse" STDIO = "stdio" +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class ModelCapabilitiesLimitsVision: + """Vision-specific limits""" + + max_prompt_image_size: int + """Maximum image size in bytes""" + + max_prompt_images: int + """Maximum number of images per prompt""" + + supported_media_types: list[str] + """MIME types the model accepts""" + + @staticmethod + def from_dict(obj: Any) -> 'ModelCapabilitiesLimitsVision': + assert isinstance(obj, dict) + max_prompt_image_size = from_int(obj.get("max_prompt_image_size")) + max_prompt_images = from_int(obj.get("max_prompt_images")) + supported_media_types = from_list(from_str, obj.get("supported_media_types")) + return ModelCapabilitiesLimitsVision(max_prompt_image_size, max_prompt_images, supported_media_types) + + def to_dict(self) -> dict: + result: dict = {} + result["max_prompt_image_size"] = from_int(self.max_prompt_image_size) + result["max_prompt_images"] = from_int(self.max_prompt_images) + result["supported_media_types"] = from_list(from_str, self.supported_media_types) + return result + +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class ModelArtifactDetails: + """Provider-reported model artifact details. + + Provider-reported model artifact metadata. + """ + architecture: str | None = None + """Provider-reported model architecture.""" + + families: list[str] | None = None + """Provider-reported model families.""" + + family: str | None = None + """Primary model family.""" + + format: str | None = None + """Artifact format, such as `gguf`.""" + + parameter_size: str | None = None + """Provider-reported parameter count label.""" + + quantization: str | None = None + """Provider-reported quantization label.""" + + tokenizer: str | None = None + """Provider-reported tokenizer.""" + + @staticmethod + def from_dict(obj: Any) -> 'ModelArtifactDetails': + assert isinstance(obj, dict) + architecture = from_union([from_str, from_none], obj.get("architecture")) + families = from_union([lambda x: from_list(from_str, x), from_none], obj.get("families")) + family = from_union([from_str, from_none], obj.get("family")) + format = from_union([from_str, from_none], obj.get("format")) + parameter_size = from_union([from_str, from_none], obj.get("parameterSize")) + quantization = from_union([from_str, from_none], obj.get("quantization")) + tokenizer = from_union([from_str, from_none], obj.get("tokenizer")) + return ModelArtifactDetails(architecture, families, family, format, parameter_size, quantization, tokenizer) + + def to_dict(self) -> dict: + result: dict = {} + if self.architecture is not None: + result["architecture"] = from_union([from_str, from_none], self.architecture) + if self.families is not None: + result["families"] = from_union([lambda x: from_list(from_str, x), from_none], self.families) + if self.family is not None: + result["family"] = from_union([from_str, from_none], self.family) + if self.format is not None: + result["format"] = from_union([from_str, from_none], self.format) + if self.parameter_size is not None: + result["parameterSize"] = from_union([from_str, from_none], self.parameter_size) + if self.quantization is not None: + result["quantization"] = from_union([from_str, from_none], self.quantization) + if self.tokenizer is not None: + result["tokenizer"] = from_union([from_str, from_none], self.tokenizer) + return result + +# Experimental: this type is part of an experimental API and may change or be removed. +class ModelProviderProvenanceSource(Enum): + """Kind of component that supplied the adapter. + + Kind of component that supplied a provider adapter or row. Attribution does not confer + authority. + + Kind of component that supplied the adapter. Attribution does not confer authority. + """ + BUILT_IN = "builtIn" + CONFIGURED = "configured" + CUSTOM = "custom" + EXTENSION = "extension" + +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class ModelProviderWarning: + """A non-fatal provider observation warning.""" + + code: str + """Machine-readable warning code.""" + + message: str + """Human-readable warning message.""" + + @staticmethod + def from_dict(obj: Any) -> 'ModelProviderWarning': + assert isinstance(obj, dict) + code = from_str(obj.get("code")) + message = from_str(obj.get("message")) + return ModelProviderWarning(code, message) + + def to_dict(self) -> dict: + result: dict = {} + result["code"] = from_str(self.code) + result["message"] = from_str(self.message) + return result + +# Experimental: this type is part of an experimental API and may change or be removed. +class ModelProviderOperationOutcomeCode(Enum): + """Machine-readable operation outcome. + + Typed outcome for a provider operation. + """ + ABSENT = "absent" + FAILED = "failed" + SUCCESS = "success" + UNREACHABLE = "unreachable" + # Experimental: this type is part of an experimental API and may change or be removed. @dataclass class EnqueueCommandParams: @@ -3287,20 +3474,6 @@ def to_dict(self) -> dict: result["displayText"] = from_union([from_none, from_str], self.display_text) return result -# Experimental: this type is part of an experimental API and may change or be removed. -class EntraTokenInteraction(Enum): - """Whether the broker may show interaction. - - How far OneAuth may go to acquire the requested token. - """ - FORCE_INTERACTIVE = "force-interactive" - INTERACTIVE = "interactive" - SILENT = "silent" - -class EntraTokenAcquireResultStatus(Enum): - INTERACTION_REQUIRED = "interaction-required" - OK = "ok" - class EnvAuthInfoType(Enum): ENV = "env" @@ -8214,35 +8387,6 @@ def to_dict(self) -> dict: result["outputPrice"] = from_union([to_float, from_none], self.output_price) return result -# Experimental: this type is part of an experimental API and may change or be removed. -@dataclass -class ModelCapabilitiesLimitsVision: - """Vision-specific limits""" - - max_prompt_image_size: int - """Maximum image size in bytes""" - - max_prompt_images: int - """Maximum number of images per prompt""" - - supported_media_types: list[str] - """MIME types the model accepts""" - - @staticmethod - def from_dict(obj: Any) -> 'ModelCapabilitiesLimitsVision': - assert isinstance(obj, dict) - max_prompt_image_size = from_int(obj.get("max_prompt_image_size")) - max_prompt_images = from_int(obj.get("max_prompt_images")) - supported_media_types = from_list(from_str, obj.get("supported_media_types")) - return ModelCapabilitiesLimitsVision(max_prompt_image_size, max_prompt_images, supported_media_types) - - def to_dict(self) -> dict: - result: dict = {} - result["max_prompt_image_size"] = from_int(self.max_prompt_image_size) - result["max_prompt_images"] = from_int(self.max_prompt_images) - result["supported_media_types"] = from_list(from_str, self.supported_media_types) - return result - # Experimental: this type is part of an experimental API and may change or be removed. @dataclass class ModelMessage: @@ -8346,6 +8490,277 @@ def to_dict(self) -> dict: result["supported_media_types"] = from_union([lambda x: from_list(from_str, x), from_none], self.supported_media_types) return result +# Experimental: this type is part of an experimental API and may change or be removed. +class ModelProviderAutomaticDiscoveryMode(Enum): + """Whether automatic discovery is allowed, limited to configured providers, or + explicit-only. + + When the runtime may run an adapter without an explicit user action. + """ + AUTOMATIC = "automatic" + CONFIGURED_ONLY = "configuredOnly" + EXPLICIT = "explicit" + +# Experimental: this type is part of an experimental API and may change or be removed. +class ModelProviderDiscoveryNetworkScope(Enum): + """Maximum network scope used by this adapter during discovery. + + Network reach an adapter may use during discovery. + """ + CONFIGURED_ENDPOINT_ONLY = "configuredEndpointOnly" + INTERNET = "internet" + LOCAL_NETWORK = "localNetwork" + LOOPBACK_ONLY = "loopbackOnly" + NONE = "none" + +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class ModelProviderAdapterOperationDescriptor: + """An operation supported by a model-provider adapter.""" + + name: str + """Supported operation name: `discover`, `getStatus`, or `models.list`. Unknown names and + duplicate declarations are rejected. + """ + input_schema: Any = None + """Optional self-contained JSON Schema Draft 7 for non-null discovery input. Only supported + on discover. No external references are resolved. Omitted or null input selects defaults + when requiresInput is false. Without a schema, the adapter validates supplied input. + """ + + @staticmethod + def from_dict(obj: Any) -> 'ModelProviderAdapterOperationDescriptor': + assert isinstance(obj, dict) + name = from_str(obj.get("name")) + input_schema = obj.get("inputSchema") + return ModelProviderAdapterOperationDescriptor(name, input_schema) + + def to_dict(self) -> dict: + result: dict = {} + result["name"] = from_str(self.name) + if self.input_schema is not None: + result["inputSchema"] = self.input_schema + return result + +# Experimental: this type is part of an experimental API and may change or be removed. +class ModelProviderConfigurationDisposition(Enum): + """Whether a planned configuration entry is new or already present in the session registry. + + Whether `model` still needs to be registered. When `alreadyConfigured`, `selectionId` is + already registered and the caller can select it without adding anything. + + Whether `provider` still needs to be registered. When `alreadyConfigured`, a provider + with the same endpoint is already registered and `provider` restates it under its + existing name; adding it again is rejected as a duplicate. + """ + ALREADY_CONFIGURED = "alreadyConfigured" + CREATE = "create" + +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class SystemMessageBlock: + content: str + """Text content for this system-message block.""" + + cache_breakpoint: bool | None = None + """Whether providers with explicit prompt caching should place a cache breakpoint after this + block. + """ + is_static: bool | None = None + """Whether the block is static and may be cached independently of dynamic prompt content.""" + + @staticmethod + def from_dict(obj: Any) -> 'SystemMessageBlock': + assert isinstance(obj, dict) + content = from_str(obj.get("content")) + cache_breakpoint = from_union([from_bool, from_none], obj.get("cacheBreakpoint")) + is_static = from_union([from_bool, from_none], obj.get("isStatic")) + return SystemMessageBlock(content, cache_breakpoint, is_static) + + def to_dict(self) -> dict: + result: dict = {} + result["content"] = from_str(self.content) + if self.cache_breakpoint is not None: + result["cacheBreakpoint"] = from_union([from_bool, from_none], self.cache_breakpoint) + if self.is_static is not None: + result["isStatic"] = from_union([from_bool, from_none], self.is_static) + return result + +class ProtocolMode(Enum): + """Append-mode discriminator. Omission also selects append mode. + + Replace-mode discriminator. + + Customize-mode discriminator. + """ + APPEND = "append" + CUSTOMIZE = "customize" + REPLACE = "replace" + +class ProtocolSectionOverrideAction(Enum): + """Declarative operation applied to the section.""" + + APPEND = "append" + PREPEND = "prepend" + PRESERVE = "preserve" + REMOVE = "remove" + REPLACE = "replace" + TRANSFORM = "transform" + +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class ProviderConfigAzure: + """Azure authentication configuration for the provider. + + Azure-specific provider options. + """ + api_version: str | None = None + """API version. When set, uses the versioned deployment route. When omitted, uses the GA + versionless v1 route. + """ + + @staticmethod + def from_dict(obj: Any) -> 'ProviderConfigAzure': + assert isinstance(obj, dict) + api_version = from_union([from_str, from_none], obj.get("apiVersion")) + return ProviderConfigAzure(api_version) + + def to_dict(self) -> dict: + result: dict = {} + if self.api_version is not None: + result["apiVersion"] = from_union([from_str, from_none], self.api_version) + return result + +# Experimental: this type is part of an experimental API and may change or be removed. +class ProviderConfigModelProvider(Enum): + """The product serving the provider's models, reported in telemetry as `model_provider`. + Only affects telemetry. + + The product serving the model, reported in telemetry as `model_provider`. + + The product serving the model, reported in telemetry as `model_provider`. Set it when + `type` alone cannot identify the product, such as Ollama or LM Studio behind an + OpenAI-compatible endpoint. Only affects telemetry. + """ + ANTHROPIC = "anthropic" + AZURE_OPENAI = "azure_openai" + FOUNDRY_LOCAL = "foundry_local" + LLAMA_CPP = "llama_cpp" + LM_STUDIO = "lm_studio" + OLLAMA = "ollama" + OPENAI = "openai" + +# Experimental: this type is part of an experimental API and may change or be removed. +class ProviderTransport(Enum): + """Transport used to communicate with the provider. + + Provider transport. Defaults to "http". + + Transport to use for inference against this instance. + + Transport to be used for provider requests. + """ + HTTP = "http" + WEBSOCKETS = "websockets" + +# Experimental: this type is part of an experimental API and may change or be removed. +class ProviderType(Enum): + """Provider protocol family. + + Provider type. Defaults to "openai" for generic OpenAI-compatible APIs. + + Provider family to use for inference against this instance. + + Provider family. Matches the `type` field of a BYOK provider config. + """ + ANTHROPIC = "anthropic" + AZURE = "azure" + OPENAI = "openai" + +# Experimental: this type is part of an experimental API and may change or be removed. +class ProviderWireAPI(Enum): + """Wire API used to communicate with the provider. + + Wire API format (openai/azure only). Defaults to "completions". + + Wire API to use for inference against this instance, when required by the provider + family. + + Wire API to be used, when required for the provider type. + """ + COMPLETIONS = "completions" + RESPONSES = "responses" + +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class ModelProviderDiscoverRequest: + """Provider discovery parameters.""" + + adapter_id: str + """Opaque adapter identity returned by `session.providers.getCatalog`.""" + + input: Any = None + """Provider-specific JSON input. Omission or null selects adapter defaults unless + requiresInput is true. Non-null input is validated against the advertised Draft 7 schema + when present; otherwise validation belongs to the adapter. + """ + + @staticmethod + def from_dict(obj: Any) -> 'ModelProviderDiscoverRequest': + assert isinstance(obj, dict) + adapter_id = from_str(obj.get("adapterId")) + input = obj.get("input") + return ModelProviderDiscoverRequest(adapter_id, input) + + def to_dict(self) -> dict: + result: dict = {} + result["adapterId"] = from_str(self.adapter_id) + if self.input is not None: + result["input"] = self.input + return result + +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class ModelProviderInstanceReference: + """Self-contained reference for subsequent provider operations. + + Serializable reference to a discovered provider instance. + + Provider instance reference returned by discovery. + """ + adapter_id: str + """Stable opaque identity of the adapter that owns this reference. Must be present in the + target session's effective catalog. + """ + id: str + """Stable instance identifier derived by the provider adapter, such as + `ollama:{normalizedEndpoint}`. + """ + management_endpoint: str + """Absolute provider management URI. The adapter validates normalization, supported schemes, + and permission to access it against its bound configuration; a reference does not grant + authority. + """ + provider_kind: str + """Descriptive provider family. Must match the selected adapter; not a routing key.""" + + @staticmethod + def from_dict(obj: Any) -> 'ModelProviderInstanceReference': + assert isinstance(obj, dict) + adapter_id = from_str(obj.get("adapterId")) + id = from_str(obj.get("id")) + management_endpoint = from_str(obj.get("managementEndpoint")) + provider_kind = from_str(obj.get("providerKind")) + return ModelProviderInstanceReference(adapter_id, id, management_endpoint, provider_kind) + + def to_dict(self) -> dict: + result: dict = {} + result["adapterId"] = from_str(self.adapter_id) + result["id"] = from_str(self.id) + result["managementEndpoint"] = from_str(self.management_endpoint) + result["providerKind"] = from_str(self.provider_kind) + return result + # Experimental: this type is part of an experimental API and may change or be removed. @dataclass class ModelSetAllowedModelsResult: @@ -8597,83 +9012,6 @@ def to_dict(self) -> dict: result["name"] = from_str(self.name) return result -# Experimental: this type is part of an experimental API and may change or be removed. -@dataclass -class ProviderConfigAzure: - """Azure authentication configuration for the provider. - - Azure-specific provider options. - """ - api_version: str | None = None - """API version. When set, uses the versioned deployment route. When omitted, uses the GA - versionless v1 route. - """ - - @staticmethod - def from_dict(obj: Any) -> 'ProviderConfigAzure': - assert isinstance(obj, dict) - api_version = from_union([from_str, from_none], obj.get("apiVersion")) - return ProviderConfigAzure(api_version) - - def to_dict(self) -> dict: - result: dict = {} - if self.api_version is not None: - result["apiVersion"] = from_union([from_str, from_none], self.api_version) - return result - -# Experimental: this type is part of an experimental API and may change or be removed. -class ProviderConfigModelProvider(Enum): - """The product serving the provider's models, reported in telemetry as `model_provider`. - Only affects telemetry. - - The product serving the model, reported in telemetry as `model_provider`. - - The product serving the model, reported in telemetry as `model_provider`. Set it when - `type` alone cannot identify the product, such as Ollama or LM Studio behind an - OpenAI-compatible endpoint. Only affects telemetry. - """ - ANTHROPIC = "anthropic" - AZURE_OPENAI = "azure_openai" - FOUNDRY_LOCAL = "foundry_local" - LLAMA_CPP = "llama_cpp" - LM_STUDIO = "lm_studio" - OLLAMA = "ollama" - OPENAI = "openai" - -# Experimental: this type is part of an experimental API and may change or be removed. -class ProviderTransport(Enum): - """Transport used to communicate with the provider. - - Provider transport. Defaults to "http". - - Transport to be used for provider requests. - """ - HTTP = "http" - WEBSOCKETS = "websockets" - -# Experimental: this type is part of an experimental API and may change or be removed. -class ProviderType(Enum): - """Provider protocol family. - - Provider type. Defaults to "openai" for generic OpenAI-compatible APIs. - - Provider family. Matches the `type` field of a BYOK provider config. - """ - ANTHROPIC = "anthropic" - AZURE = "azure" - OPENAI = "openai" - -# Experimental: this type is part of an experimental API and may change or be removed. -class ProviderWireAPI(Enum): - """Wire API used to communicate with the provider. - - Wire API format (openai/azure only). Defaults to "completions". - - Wire API to be used, when required for the provider type. - """ - COMPLETIONS = "completions" - RESPONSES = "responses" - # Experimental: this type is part of an experimental API and may change or be removed. @dataclass class OptionsUpdateAdditionalContentExclusionPolicyRuleSource: @@ -10004,16 +10342,6 @@ class ProtocolReplaceMode(Enum): REPLACE = "replace" -class ProtocolSectionOverrideAction(Enum): - """Declarative operation applied to the section.""" - - APPEND = "append" - PREPEND = "prepend" - PRESERVE = "preserve" - REMOVE = "remove" - REPLACE = "replace" - TRANSFORM = "transform" - # Experimental: this type is part of an experimental API and may change or be removed. class ProtocolStaticSectionAction(Enum): """Declarative operation applied to the section.""" @@ -10023,47 +10351,6 @@ class ProtocolStaticSectionAction(Enum): REMOVE = "remove" REPLACE = "replace" -# Experimental: this type is part of an experimental API and may change or be removed. -@dataclass -class SystemMessageBlock: - content: str - """Text content for this system-message block.""" - - cache_breakpoint: bool | None = None - """Whether providers with explicit prompt caching should place a cache breakpoint after this - block. - """ - is_static: bool | None = None - """Whether the block is static and may be cached independently of dynamic prompt content.""" - - @staticmethod - def from_dict(obj: Any) -> 'SystemMessageBlock': - assert isinstance(obj, dict) - content = from_str(obj.get("content")) - cache_breakpoint = from_union([from_bool, from_none], obj.get("cacheBreakpoint")) - is_static = from_union([from_bool, from_none], obj.get("isStatic")) - return SystemMessageBlock(content, cache_breakpoint, is_static) - - def to_dict(self) -> dict: - result: dict = {} - result["content"] = from_str(self.content) - if self.cache_breakpoint is not None: - result["cacheBreakpoint"] = from_union([from_bool, from_none], self.cache_breakpoint) - if self.is_static is not None: - result["isStatic"] = from_union([from_bool, from_none], self.is_static) - return result - -class ProtocolMode(Enum): - """Append-mode discriminator. Omission also selects append mode. - - Replace-mode discriminator. - - Customize-mode discriminator. - """ - APPEND = "append" - CUSTOMIZE = "customize" - REPLACE = "replace" - # Experimental: this type is part of an experimental API and may change or be removed. @dataclass class ProviderAddResult: @@ -11647,6 +11934,36 @@ def to_dict(self) -> dict: result["reason"] = from_union([from_str, from_none], self.reason) return result +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class SandboxProxyCACreateResult: + """Result of creating the persistent certificate authority of the sandbox credential proxy.""" + + certificate_path: str + """Absolute path of the public certificate of the certificate authority, in PEM format.""" + + @staticmethod + def from_dict(obj: Any) -> 'SandboxProxyCACreateResult': + assert isinstance(obj, dict) + certificate_path = from_str(obj.get("certificatePath")) + return SandboxProxyCACreateResult(certificate_path) + + def to_dict(self) -> dict: + result: dict = {} + result["certificatePath"] = from_str(self.certificate_path) + return result + +# Experimental: this type is part of an experimental API and may change or be removed. +class SandboxProxyCAState(Enum): + """State of the persistent certificate authority of the sandbox credential proxy. + + The state of the certificate authority. + """ + ERROR = "error" + INSTALLED = "installed" + NOT_INSTALLED = "notInstalled" + UNSUPPORTED = "unsupported" + # Experimental: this type is part of an experimental API and may change or be removed. class SandboxSessionChange(Enum): """A session-scoped sandbox transition applied while handling a slash command @@ -12497,6 +12814,30 @@ def to_dict(self) -> dict: result["recursive"] = from_union([from_bool, from_none], self.recursive) return result +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class SessionFSReadFileBytesRequest: + """Path of the binary file to read from the client-provided session filesystem.""" + + path: str + """Path using SessionFs conventions""" + + session_id: str + """Target session identifier""" + + @staticmethod + def from_dict(obj: Any) -> 'SessionFSReadFileBytesRequest': + assert isinstance(obj, dict) + path = from_str(obj.get("path")) + session_id = from_str(obj.get("sessionId")) + return SessionFSReadFileBytesRequest(path, session_id) + + def to_dict(self) -> dict: + result: dict = {} + result["path"] = from_str(self.path) + result["sessionId"] = from_str(self.session_id) + return result + # Experimental: this type is part of an experimental API and may change or be removed. @dataclass class SessionFSReadFileRequest: @@ -12645,14 +12986,21 @@ class SessionFSSetProviderCapabilities: sqlite: bool | None = None """Whether the provider supports SQLite query/exists operations""" + binary: bool | None = field(default=None, kw_only=True) + """Whether the provider supports binary reads and writes through sessionFs.readFileBytes and + sessionFs.writeFileBytes + """ @staticmethod def from_dict(obj: Any) -> 'SessionFSSetProviderCapabilities': assert isinstance(obj, dict) + binary = from_union([from_bool, from_none], obj.get("binary")) sqlite = from_union([from_bool, from_none], obj.get("sqlite")) - return SessionFSSetProviderCapabilities(sqlite) + return SessionFSSetProviderCapabilities(sqlite, binary=binary) def to_dict(self) -> dict: result: dict = {} + if self.binary is not None: + result["binary"] = from_union([from_bool, from_none], self.binary) if self.sqlite is not None: result["sqlite"] = from_union([from_bool, from_none], self.sqlite) return result @@ -12766,6 +13114,42 @@ def to_dict(self) -> dict: result["sessionId"] = from_str(self.session_id) return result +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class SessionFSWriteFileBytesRequest: + """File path, standard-base64-encoded bytes to write, and optional mode for the + client-provided session filesystem. + """ + content: str + """Exact file bytes encoded as standard base64""" + + path: str + """Path using SessionFs conventions""" + + session_id: str + """Target session identifier""" + + mode: int | None = None + """Optional POSIX-style mode for newly created files""" + + @staticmethod + def from_dict(obj: Any) -> 'SessionFSWriteFileBytesRequest': + assert isinstance(obj, dict) + content = from_str(obj.get("content")) + path = from_str(obj.get("path")) + session_id = from_str(obj.get("sessionId")) + mode = from_union([from_int, from_none], obj.get("mode")) + return SessionFSWriteFileBytesRequest(content, path, session_id, mode) + + def to_dict(self) -> dict: + result: dict = {} + result["content"] = from_str(self.content) + result["path"] = from_str(self.path) + result["sessionId"] = from_str(self.session_id) + if self.mode is not None: + result["mode"] = from_union([from_int, from_none], self.mode) + return result + # Experimental: this type is part of an experimental API and may change or be removed. @dataclass class SessionFSWriteFileRequest: @@ -17863,6 +18247,10 @@ class ModelCapabilitiesSupports: reasoning_effort: bool | None = None """Whether this model supports reasoning effort configuration""" + thinking: bool | None = None + """Whether the model supports provider-native thinking. Independent of configurable + reasoning effort; omission means unknown. + """ tool_calls: bool | None = None """Whether this model supports canonical tool calling""" @@ -17874,9 +18262,10 @@ def from_dict(obj: Any) -> 'ModelCapabilitiesSupports': assert isinstance(obj, dict) adaptive_thinking = from_union([AdaptiveThinkingSupport, from_none], obj.get("adaptive_thinking")) reasoning_effort = from_union([from_bool, from_none], obj.get("reasoningEffort")) + thinking = from_union([from_bool, from_none], obj.get("thinking")) tool_calls = from_union([from_bool, from_none], obj.get("toolCalls")) vision = from_union([from_bool, from_none], obj.get("vision")) - return ModelCapabilitiesSupports(adaptive_thinking, reasoning_effort, tool_calls, vision) + return ModelCapabilitiesSupports(adaptive_thinking, reasoning_effort, thinking, tool_calls, vision) def to_dict(self) -> dict: result: dict = {} @@ -17884,6 +18273,8 @@ def to_dict(self) -> dict: result["adaptive_thinking"] = from_union([lambda x: to_enum(AdaptiveThinkingSupport, x), from_none], self.adaptive_thinking) if self.reasoning_effort is not None: result["reasoningEffort"] = from_union([from_bool, from_none], self.reasoning_effort) + if self.thinking is not None: + result["thinking"] = from_union([from_bool, from_none], self.thinking) if self.tool_calls is not None: result["toolCalls"] = from_union([from_bool, from_none], self.tool_calls) if self.vision is not None: @@ -20303,6 +20694,36 @@ def to_dict(self) -> dict: result["models"] = from_list(from_str, self.models) return result +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class CustomizationReloadOutcome: + """Result of one customization reload component.""" + + status: CustomizationReloadStatus + """Whether the component reloaded, was skipped, or failed""" + + subsystem: CustomizationReloadSubsystem + """Component whose reload was attempted or skipped""" + + detail: str | None = None + """Reason for a skipped component or description of a failure, when available""" + + @staticmethod + def from_dict(obj: Any) -> 'CustomizationReloadOutcome': + assert isinstance(obj, dict) + status = CustomizationReloadStatus(obj.get("status")) + subsystem = CustomizationReloadSubsystem(obj.get("subsystem")) + detail = from_union([from_none, from_str], obj.get("detail")) + return CustomizationReloadOutcome(status, subsystem, detail) + + def to_dict(self) -> dict: + result: dict = {} + result["status"] = to_enum(CustomizationReloadStatus, self.status) + result["subsystem"] = to_enum(CustomizationReloadSubsystem, self.subsystem) + if self.detail is not None: + result["detail"] = from_union([from_none, from_str], self.detail) + return result + # Experimental: this type is part of an experimental API and may change or be removed. @dataclass class DebugCollectLogsCollectedEntry: @@ -20696,86 +21117,154 @@ def to_dict(self) -> dict: # Experimental: this type is part of an experimental API and may change or be removed. @dataclass -class EntraTokenAcquireRequest: - """OneAuth token request supplied by a trusted host application.""" +class ModelCapabilitiesLimits: + """Token limits for prompts, outputs, and context window""" + + max_context_window_tokens: int | None = None + """Maximum total context window size in tokens""" - client_id: str - """Public client application id.""" + max_output_tokens: int | None = None + """Maximum number of output/completion tokens""" - interaction: EntraTokenInteraction - """Whether the broker may show interaction.""" + max_prompt_tokens: int | None = None + """Maximum number of prompt/input tokens""" - redirect_uri: str - """Broker redirect URI registered for the client. Required: the OneAuth broker validates a - non-empty, registered redirect URI for the public client (MSAL broker registration), so - this is not a browser-flow vestige and cannot be omitted. + vision: ModelCapabilitiesLimitsVision | None = None + """Vision-specific limits""" + + @staticmethod + def from_dict(obj: Any) -> 'ModelCapabilitiesLimits': + assert isinstance(obj, dict) + max_context_window_tokens = from_union([from_int, from_none], obj.get("max_context_window_tokens")) + max_output_tokens = from_union([from_int, from_none], obj.get("max_output_tokens")) + max_prompt_tokens = from_union([from_int, from_none], obj.get("max_prompt_tokens")) + vision = from_union([ModelCapabilitiesLimitsVision.from_dict, from_none], obj.get("vision")) + return ModelCapabilitiesLimits(max_context_window_tokens, max_output_tokens, max_prompt_tokens, vision) + + def to_dict(self) -> dict: + result: dict = {} + if self.max_context_window_tokens is not None: + result["max_context_window_tokens"] = from_union([from_int, from_none], self.max_context_window_tokens) + if self.max_output_tokens is not None: + result["max_output_tokens"] = from_union([from_int, from_none], self.max_output_tokens) + if self.max_prompt_tokens is not None: + result["max_prompt_tokens"] = from_union([from_int, from_none], self.max_prompt_tokens) + if self.vision is not None: + result["vision"] = from_union([lambda x: to_class(ModelCapabilitiesLimitsVision, x), from_none], self.vision) + return result + +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class ModelProviderProvenance: + """Attribution for the adapter that produced this model row. + + Attribution for the adapter that produced a provider row. + + Attribution for the adapter that produced this instance. """ - scopes: list[str] - """Exact delegated scopes to request.""" + adapter_id: str + """Stable opaque adapter identity from the effective catalog. Treat this as a whole + identifier, not a parseable owner or kind. + """ + provider_kind: str + """Descriptive provider family that produced this row; not a routing key.""" + + source: ModelProviderProvenanceSource + """Kind of component that supplied the adapter.""" - tenant_id: str - """Tenant id or tenant selector, such as common or organizations.""" + owner_display_name: str | None = None + """Human-readable contributor name, not the adapter display name.""" - access_token_to_renew: str | None = None - """Previously rejected token that OneAuth must bypass during renewal.""" + owner_id: str | None = None + """Stable contributor identifier when the adapter has an owner outside the runtime. + Independent of the contribution mechanism and not a routing key. + """ @staticmethod - def from_dict(obj: Any) -> 'EntraTokenAcquireRequest': + def from_dict(obj: Any) -> 'ModelProviderProvenance': assert isinstance(obj, dict) - client_id = from_str(obj.get("clientId")) - interaction = EntraTokenInteraction(obj.get("interaction")) - redirect_uri = from_str(obj.get("redirectUri")) - scopes = from_list(from_str, obj.get("scopes")) - tenant_id = from_str(obj.get("tenantId")) - access_token_to_renew = from_union([from_str, from_none], obj.get("accessTokenToRenew")) - return EntraTokenAcquireRequest(client_id, interaction, redirect_uri, scopes, tenant_id, access_token_to_renew) + adapter_id = from_str(obj.get("adapterId")) + provider_kind = from_str(obj.get("providerKind")) + source = ModelProviderProvenanceSource(obj.get("source")) + owner_display_name = from_union([from_str, from_none], obj.get("ownerDisplayName")) + owner_id = from_union([from_str, from_none], obj.get("ownerId")) + return ModelProviderProvenance(adapter_id, provider_kind, source, owner_display_name, owner_id) def to_dict(self) -> dict: result: dict = {} - result["clientId"] = from_str(self.client_id) - result["interaction"] = to_enum(EntraTokenInteraction, self.interaction) - result["redirectUri"] = from_str(self.redirect_uri) - result["scopes"] = from_list(from_str, self.scopes) - result["tenantId"] = from_str(self.tenant_id) - if self.access_token_to_renew is not None: - result["accessTokenToRenew"] = from_union([from_str, from_none], self.access_token_to_renew) + result["adapterId"] = from_str(self.adapter_id) + result["providerKind"] = from_str(self.provider_kind) + result["source"] = to_enum(ModelProviderProvenanceSource, self.source) + if self.owner_display_name is not None: + result["ownerDisplayName"] = from_union([from_str, from_none], self.owner_display_name) + if self.owner_id is not None: + result["ownerId"] = from_union([from_str, from_none], self.owner_id) return result # Experimental: this type is part of an experimental API and may change or be removed. @dataclass -class EntraTokenAcquireResult: - """Result of a OneAuth token acquisition.""" +class ModelProviderAttribution: + """Attribution for the adapter itself. - status: EntraTokenAcquireResultStatus - """OneAuth token acquisition outcome discriminator.""" + Contributor attribution, independent of routing identity and authorization. + """ + source: ModelProviderProvenanceSource + """Kind of component that supplied the adapter. Attribution does not confer authority.""" - access_token: str | None = None - """Opaque access token.""" + owner_display_name: str | None = None + """Human-readable contributor name, not the adapter display name.""" - account_id: str | None = None - """Opaque OneAuth account id, when supplied by the broker.""" + owner_id: str | None = None + """Stable contributor identifier. Required and nonblank for extension and custom sources; + optional for built-in and configured sources. Does not grant authority. + """ - expires_on_timestamp: float | None = None - """Expiry as milliseconds since Unix epoch, when supplied by OneAuth.""" + @staticmethod + def from_dict(obj: Any) -> 'ModelProviderAttribution': + assert isinstance(obj, dict) + source = ModelProviderProvenanceSource(obj.get("source")) + owner_display_name = from_union([from_str, from_none], obj.get("ownerDisplayName")) + owner_id = from_union([from_str, from_none], obj.get("ownerId")) + return ModelProviderAttribution(source, owner_display_name, owner_id) + + def to_dict(self) -> dict: + result: dict = {} + result["source"] = to_enum(ModelProviderProvenanceSource, self.source) + if self.owner_display_name is not None: + result["ownerDisplayName"] = from_union([from_str, from_none], self.owner_display_name) + if self.owner_id is not None: + result["ownerId"] = from_union([from_str, from_none], self.owner_id) + return result + +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class ModelProviderOperationOutcome: + """Typed operation outcome. + + Typed provider-operation outcome. Use the code for control flow and the optional message + for display. + + Typed operation outcome. Passive discovery can return `absent` with an empty instance + list. + """ + code: ModelProviderOperationOutcomeCode + """Machine-readable operation outcome.""" + + message: str | None = None + """Human-readable detail for non-success outcomes.""" @staticmethod - def from_dict(obj: Any) -> 'EntraTokenAcquireResult': + def from_dict(obj: Any) -> 'ModelProviderOperationOutcome': assert isinstance(obj, dict) - status = EntraTokenAcquireResultStatus(obj.get("status")) - access_token = from_union([from_str, from_none], obj.get("accessToken")) - account_id = from_union([from_str, from_none], obj.get("accountId")) - expires_on_timestamp = from_union([from_float, from_none], obj.get("expiresOnTimestamp")) - return EntraTokenAcquireResult(status, access_token, account_id, expires_on_timestamp) + code = ModelProviderOperationOutcomeCode(obj.get("code")) + message = from_union([from_str, from_none], obj.get("message")) + return ModelProviderOperationOutcome(code, message) def to_dict(self) -> dict: result: dict = {} - result["status"] = to_enum(EntraTokenAcquireResultStatus, self.status) - if self.access_token is not None: - result["accessToken"] = from_union([from_str, from_none], self.access_token) - if self.account_id is not None: - result["accountId"] = from_union([from_str, from_none], self.account_id) - if self.expires_on_timestamp is not None: - result["expiresOnTimestamp"] = from_union([to_float, from_none], self.expires_on_timestamp) + result["code"] = to_enum(ModelProviderOperationOutcomeCode, self.code) + if self.message is not None: + result["message"] = from_union([from_str, from_none], self.message) return result # Experimental: this type is part of an experimental API and may change or be removed. @@ -24414,44 +24903,6 @@ def to_dict(self) -> dict: result["outputPrice"] = from_union([to_float, from_none], self.output_price) return result -# Experimental: this type is part of an experimental API and may change or be removed. -@dataclass -class ModelCapabilitiesLimits: - """Token limits for prompts, outputs, and context window""" - - max_context_window_tokens: int | None = None - """Maximum total context window size in tokens""" - - max_output_tokens: int | None = None - """Maximum number of output/completion tokens""" - - max_prompt_tokens: int | None = None - """Maximum number of prompt/input tokens""" - - vision: ModelCapabilitiesLimitsVision | None = None - """Vision-specific limits""" - - @staticmethod - def from_dict(obj: Any) -> 'ModelCapabilitiesLimits': - assert isinstance(obj, dict) - max_context_window_tokens = from_union([from_int, from_none], obj.get("max_context_window_tokens")) - max_output_tokens = from_union([from_int, from_none], obj.get("max_output_tokens")) - max_prompt_tokens = from_union([from_int, from_none], obj.get("max_prompt_tokens")) - vision = from_union([ModelCapabilitiesLimitsVision.from_dict, from_none], obj.get("vision")) - return ModelCapabilitiesLimits(max_context_window_tokens, max_output_tokens, max_prompt_tokens, vision) - - def to_dict(self) -> dict: - result: dict = {} - if self.max_context_window_tokens is not None: - result["max_context_window_tokens"] = from_union([from_int, from_none], self.max_context_window_tokens) - if self.max_output_tokens is not None: - result["max_output_tokens"] = from_union([from_int, from_none], self.max_output_tokens) - if self.max_prompt_tokens is not None: - result["max_prompt_tokens"] = from_union([from_int, from_none], self.max_prompt_tokens) - if self.vision is not None: - result["vision"] = from_union([lambda x: to_class(ModelCapabilitiesLimitsVision, x), from_none], self.vision) - return result - # Experimental: this type is part of an experimental API and may change or be removed. @dataclass class SessionModelPriceCategory: @@ -24541,55 +24992,76 @@ def to_dict(self) -> dict: # Experimental: this type is part of an experimental API and may change or be removed. @dataclass -class ModelSwitchAutoTierResult: - """Immediate acknowledgement and Auto preference snapshot after a switch request. This - result never implies that a pending preference committed. +class ModelProviderAutomaticDiscoveryPolicy: + """Adapter-declared policy for passive and automatic discovery. + + Adapter-declared policy that tells clients whether discovery may run automatically. """ - status: ModelSwitchAutoTierStatus - """Immediate request status. `pending` means accepted but not committed.""" + mode: ModelProviderAutomaticDiscoveryMode + """Whether automatic discovery is allowed, limited to configured providers, or explicit-only.""" - activating_auto_tier: AutoTier | None = None - """Auto preference currently claimed by an in-progress activation. Null means the activation - is returning to provider-default routing. + network_scope: ModelProviderDiscoveryNetworkScope + """Maximum network scope used by this adapter during discovery.""" + + requires_input: bool + """True when discovery requires non-null caller input. Omission or null is rejected before + adapter execution. When false, omitted or null input selects adapter defaults without + schema validation. + """ + requires_trust: bool + """True when the adapter must be enabled by a trusted owner, such as a trusted extension, + before automatic discovery may run. """ - effective_auto_tier: AutoTier | None = None - """Auto preference currently committed for the session.""" - pending_auto_tier: AutoTier | None = None - """Latest unclaimed Auto preference waiting for a future user turn.""" + @staticmethod + def from_dict(obj: Any) -> 'ModelProviderAutomaticDiscoveryPolicy': + assert isinstance(obj, dict) + mode = ModelProviderAutomaticDiscoveryMode(obj.get("mode")) + network_scope = ModelProviderDiscoveryNetworkScope(obj.get("networkScope")) + requires_input = from_bool(obj.get("requiresInput")) + requires_trust = from_bool(obj.get("requiresTrust")) + return ModelProviderAutomaticDiscoveryPolicy(mode, network_scope, requires_input, requires_trust) - superseded_auto_tier: AutoTier | None = None - """Earlier unclaimed preference replaced by this request. This can be present with either - status, including when selecting the effective preference cancels pending work. + def to_dict(self) -> dict: + result: dict = {} + result["mode"] = to_enum(ModelProviderAutomaticDiscoveryMode, self.mode) + result["networkScope"] = to_enum(ModelProviderDiscoveryNetworkScope, self.network_scope) + result["requiresInput"] = from_bool(self.requires_input) + result["requiresTrust"] = from_bool(self.requires_trust) + return result + +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class ProtocolSectionOverride: + action: ProtocolSectionOverrideAction + """Declarative operation applied to the section. + + Section override action discriminator. """ + content: str | None = None + """Optional content used by replace, append, and prepend operations.""" @staticmethod - def from_dict(obj: Any) -> 'ModelSwitchAutoTierResult': + def from_dict(obj: Any) -> 'ProtocolSectionOverride': assert isinstance(obj, dict) - status = ModelSwitchAutoTierStatus(obj.get("status")) - activating_auto_tier = from_union([AutoTier, from_none], obj.get("activatingAutoTier")) - effective_auto_tier = from_union([AutoTier, from_none], obj.get("effectiveAutoTier")) - pending_auto_tier = from_union([AutoTier, from_none], obj.get("pendingAutoTier")) - superseded_auto_tier = from_union([AutoTier, from_none], obj.get("supersededAutoTier")) - return ModelSwitchAutoTierResult(status, activating_auto_tier, effective_auto_tier, pending_auto_tier, superseded_auto_tier) + action = ProtocolSectionOverrideAction(obj.get("action")) + content = from_union([from_str, from_none], obj.get("content")) + return ProtocolSectionOverride(action, content) def to_dict(self) -> dict: result: dict = {} - result["status"] = to_enum(ModelSwitchAutoTierStatus, self.status) - if self.activating_auto_tier is not None: - result["activatingAutoTier"] = from_union([lambda x: to_enum(AutoTier, x), from_none], self.activating_auto_tier) - if self.effective_auto_tier is not None: - result["effectiveAutoTier"] = from_union([lambda x: to_enum(AutoTier, x), from_none], self.effective_auto_tier) - if self.pending_auto_tier is not None: - result["pendingAutoTier"] = from_union([lambda x: to_enum(AutoTier, x), from_none], self.pending_auto_tier) - if self.superseded_auto_tier is not None: - result["supersededAutoTier"] = from_union([lambda x: to_enum(AutoTier, x), from_none], self.superseded_auto_tier) + result["action"] = to_enum(ProtocolSectionOverrideAction, self.action) + if self.content is not None: + result["content"] = from_union([from_str, from_none], self.content) return result # Experimental: this type is part of an experimental API and may change or be removed. @dataclass class NamedProviderConfig: - """External SDK input for a named custom model provider. Ingested by the native protocol + """Provider connection prepared from the instance's inference metadata. Carries no + credential; supply one if the endpoint requires it. + + External SDK input for a named custom model provider. Ingested by the native protocol boundary before host dispatch. """ base_url: str @@ -24666,6 +25138,91 @@ def to_dict(self) -> dict: result["wireApi"] = from_union([lambda x: to_enum(ProviderWireAPI, x), from_none], self.wire_api) return result +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class ModelProviderGetStatusRequest: + """Provider status request parameters.""" + + instance: ModelProviderInstanceReference + """Provider instance reference returned by discovery.""" + + @staticmethod + def from_dict(obj: Any) -> 'ModelProviderGetStatusRequest': + assert isinstance(obj, dict) + instance = ModelProviderInstanceReference.from_dict(obj.get("instance")) + return ModelProviderGetStatusRequest(instance) + + def to_dict(self) -> dict: + result: dict = {} + result["instance"] = to_class(ModelProviderInstanceReference, self.instance) + return result + +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class ModelProviderModelsListRequest: + """Provider model inventory request parameters.""" + + instance: ModelProviderInstanceReference + """Provider instance reference returned by discovery.""" + + @staticmethod + def from_dict(obj: Any) -> 'ModelProviderModelsListRequest': + assert isinstance(obj, dict) + instance = ModelProviderInstanceReference.from_dict(obj.get("instance")) + return ModelProviderModelsListRequest(instance) + + def to_dict(self) -> dict: + result: dict = {} + result["instance"] = to_class(ModelProviderInstanceReference, self.instance) + return result + +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class ModelSwitchAutoTierResult: + """Immediate acknowledgement and Auto preference snapshot after a switch request. This + result never implies that a pending preference committed. + """ + status: ModelSwitchAutoTierStatus + """Immediate request status. `pending` means accepted but not committed.""" + + activating_auto_tier: AutoTier | None = None + """Auto preference currently claimed by an in-progress activation. Null means the activation + is returning to provider-default routing. + """ + effective_auto_tier: AutoTier | None = None + """Auto preference currently committed for the session.""" + + pending_auto_tier: AutoTier | None = None + """Latest unclaimed Auto preference waiting for a future user turn.""" + + superseded_auto_tier: AutoTier | None = None + """Earlier unclaimed preference replaced by this request. This can be present with either + status, including when selecting the effective preference cancels pending work. + """ + + @staticmethod + def from_dict(obj: Any) -> 'ModelSwitchAutoTierResult': + assert isinstance(obj, dict) + status = ModelSwitchAutoTierStatus(obj.get("status")) + activating_auto_tier = from_union([AutoTier, from_none], obj.get("activatingAutoTier")) + effective_auto_tier = from_union([AutoTier, from_none], obj.get("effectiveAutoTier")) + pending_auto_tier = from_union([AutoTier, from_none], obj.get("pendingAutoTier")) + superseded_auto_tier = from_union([AutoTier, from_none], obj.get("supersededAutoTier")) + return ModelSwitchAutoTierResult(status, activating_auto_tier, effective_auto_tier, pending_auto_tier, superseded_auto_tier) + + def to_dict(self) -> dict: + result: dict = {} + result["status"] = to_enum(ModelSwitchAutoTierStatus, self.status) + if self.activating_auto_tier is not None: + result["activatingAutoTier"] = from_union([lambda x: to_enum(AutoTier, x), from_none], self.activating_auto_tier) + if self.effective_auto_tier is not None: + result["effectiveAutoTier"] = from_union([lambda x: to_enum(AutoTier, x), from_none], self.effective_auto_tier) + if self.pending_auto_tier is not None: + result["pendingAutoTier"] = from_union([lambda x: to_enum(AutoTier, x), from_none], self.pending_auto_tier) + if self.superseded_auto_tier is not None: + result["supersededAutoTier"] = from_union([lambda x: to_enum(AutoTier, x), from_none], self.superseded_auto_tier) + return result + # Experimental: this type is part of an experimental API and may change or be removed. @dataclass class OptionsUpdateAdditionalContentExclusionPolicyRule: @@ -26641,27 +27198,30 @@ class UISessionLimitsExhaustedResponseAction(Enum): # Experimental: this type is part of an experimental API and may change or be removed. @dataclass -class ProtocolSectionOverride: - action: ProtocolSectionOverrideAction - """Declarative operation applied to the section. +class ProtocolSystemMessageReplaceConfig: + content: str + """Complete replacement system-message text.""" - Section override action discriminator. - """ - content: str | None = None - """Optional content used by replace, append, and prepend operations.""" + mode: ProtocolReplaceMode + """Replace-mode discriminator.""" + + content_blocks: list[SystemMessageBlock] | None = None + """Optional structured blocks corresponding to the replacement content.""" @staticmethod - def from_dict(obj: Any) -> 'ProtocolSectionOverride': + def from_dict(obj: Any) -> 'ProtocolSystemMessageReplaceConfig': assert isinstance(obj, dict) - action = ProtocolSectionOverrideAction(obj.get("action")) - content = from_union([from_str, from_none], obj.get("content")) - return ProtocolSectionOverride(action, content) + content = from_str(obj.get("content")) + mode = ProtocolReplaceMode(obj.get("mode")) + content_blocks = from_union([lambda x: from_list(SystemMessageBlock.from_dict, x), from_none], obj.get("contentBlocks")) + return ProtocolSystemMessageReplaceConfig(content, mode, content_blocks) def to_dict(self) -> dict: result: dict = {} - result["action"] = to_enum(ProtocolSectionOverrideAction, self.action) - if self.content is not None: - result["content"] = from_union([from_str, from_none], self.content) + result["content"] = from_str(self.content) + result["mode"] = to_enum(ProtocolReplaceMode, self.mode) + if self.content_blocks is not None: + result["contentBlocks"] = from_union([lambda x: from_list(lambda x: to_class(SystemMessageBlock, x), x), from_none], self.content_blocks) return result # Experimental: this type is part of an experimental API and may change or be removed. @@ -26687,34 +27247,6 @@ def to_dict(self) -> dict: result["content"] = from_union([from_str, from_none], self.content) return result -# Experimental: this type is part of an experimental API and may change or be removed. -@dataclass -class ProtocolSystemMessageReplaceConfig: - content: str - """Complete replacement system-message text.""" - - mode: ProtocolReplaceMode - """Replace-mode discriminator.""" - - content_blocks: list[SystemMessageBlock] | None = None - """Optional structured blocks corresponding to the replacement content.""" - - @staticmethod - def from_dict(obj: Any) -> 'ProtocolSystemMessageReplaceConfig': - assert isinstance(obj, dict) - content = from_str(obj.get("content")) - mode = ProtocolReplaceMode(obj.get("mode")) - content_blocks = from_union([lambda x: from_list(SystemMessageBlock.from_dict, x), from_none], obj.get("contentBlocks")) - return ProtocolSystemMessageReplaceConfig(content, mode, content_blocks) - - def to_dict(self) -> dict: - result: dict = {} - result["content"] = from_str(self.content) - result["mode"] = to_enum(ProtocolReplaceMode, self.mode) - if self.content_blocks is not None: - result["contentBlocks"] = from_union([lambda x: from_list(lambda x: to_class(SystemMessageBlock, x), x), from_none], self.content_blocks) - return result - # Experimental: this type is part of an experimental API and may change or be removed. @dataclass class ProviderEndpoint: @@ -27808,6 +28340,41 @@ def to_dict(self) -> dict: result["reason"] = from_union([from_str, from_none], self.reason) return result +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class SandboxProxyCAStatus: + """Status of the persistent certificate authority of the sandbox credential proxy.""" + + can_install: bool + """Whether this process can add the certificate authority to OS trust without credentials + from a different user. False where OS trust is unsupported, and on Windows when the + process cannot elevate itself to write the machine trust store. When false, do not offer + to set up the certificate authority. + """ + state: SandboxProxyCAState + """The state of the certificate authority.""" + + detail: str | None = None + """Human-readable reason for the state. On `installed` or `notInstalled`, present only when + the certificate authority must be rotated, and then says why. + """ + + @staticmethod + def from_dict(obj: Any) -> 'SandboxProxyCAStatus': + assert isinstance(obj, dict) + can_install = from_bool(obj.get("canInstall")) + state = SandboxProxyCAState(obj.get("state")) + detail = from_union([from_str, from_none], obj.get("detail")) + return SandboxProxyCAStatus(can_install, state, detail) + + def to_dict(self) -> dict: + result: dict = {} + result["canInstall"] = from_bool(self.can_install) + result["state"] = to_enum(SandboxProxyCAState, self.state) + if self.detail is not None: + result["detail"] = from_union([from_str, from_none], self.detail) + return result + # Experimental: this type is part of an experimental API and may change or be removed. @dataclass class SlashCommandTextResult: @@ -27962,18 +28529,26 @@ class SessionFSError: message: str | None = None """Free-form detail about the error, for logging/diagnostics""" + write_changed: bool | None = None + """For failed writeFile requests only: true if the provider changed the target before + failing. Omit when unknown or unchanged. + """ + @staticmethod def from_dict(obj: Any) -> 'SessionFSError': assert isinstance(obj, dict) code = SessionFSErrorCode(obj.get("code")) message = from_union([from_str, from_none], obj.get("message")) - return SessionFSError(code, message) + write_changed = from_union([from_bool, from_none], obj.get("writeChanged")) + return SessionFSError(code, message, write_changed) def to_dict(self) -> dict: result: dict = {} result["code"] = to_enum(SessionFSErrorCode, self.code) if self.message is not None: result["message"] = from_union([from_str, from_none], self.message) + if self.write_changed is not None: + result["writeChanged"] = from_union([from_bool, from_none], self.write_changed) return result # Experimental: this type is part of an experimental API and may change or be removed. @@ -32103,6 +32678,38 @@ def to_dict(self) -> dict: result["ownerType"] = from_union([from_str, from_none], self.owner_type) return result +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class CustomizationsReloadResult: + """Results of reloading discovered session customizations. Inspect outcomes for reloaded, + skipped, or failed subsystems; a rejection may follow partial mutation. Changes to the + model-facing prompt and tools apply on the next turn. + """ + errors: list[str] + """Errors from any component that could not be refreshed""" + + outcomes: list[CustomizationReloadOutcome] + """Outcome of each component in reload order; a skipped component was not configured or + loaded + """ + warnings: list[str] + """Warnings from skill discovery""" + + @staticmethod + def from_dict(obj: Any) -> 'CustomizationsReloadResult': + assert isinstance(obj, dict) + errors = from_list(from_str, obj.get("errors")) + outcomes = from_list(CustomizationReloadOutcome.from_dict, obj.get("outcomes")) + warnings = from_list(from_str, obj.get("warnings")) + return CustomizationsReloadResult(errors, outcomes, warnings) + + def to_dict(self) -> dict: + result: dict = {} + result["errors"] = from_list(from_str, self.errors) + result["outcomes"] = from_list(lambda x: to_class(CustomizationReloadOutcome, x), self.outcomes) + result["warnings"] = from_list(from_str, self.warnings) + return result + # Experimental: this type is part of an experimental API and may change or be removed. @dataclass class DebugCollectLogsResult: @@ -32284,6 +32891,64 @@ def to_dict(self) -> dict: result["warnings"] = from_list(from_str, self.warnings) return result +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class ModelProviderInstance: + """A normalized model-provider instance discovered by the runtime. + + The discovered instance that serves the model. + + Normalized provider instance. + """ + display_name: str + """Human-readable instance name.""" + + provenance: ModelProviderProvenance + """Attribution for the adapter that produced this instance.""" + + reference: ModelProviderInstanceReference + """Self-contained reference for subsequent provider operations.""" + + inference_endpoint: str | None = None + """Inference API endpoint when the provider exposes one separately from its management + endpoint. + """ + inference_transport: ProviderTransport | None = None + """Transport to use for inference against this instance.""" + + inference_type: ProviderType | None = None + """Provider family to use for inference against this instance.""" + + inference_wire_api: ProviderWireAPI | None = None + """Wire API to use for inference against this instance, when required by the provider family.""" + + @staticmethod + def from_dict(obj: Any) -> 'ModelProviderInstance': + assert isinstance(obj, dict) + display_name = from_str(obj.get("displayName")) + provenance = ModelProviderProvenance.from_dict(obj.get("provenance")) + reference = ModelProviderInstanceReference.from_dict(obj.get("reference")) + inference_endpoint = from_union([from_str, from_none], obj.get("inferenceEndpoint")) + inference_transport = from_union([ProviderTransport, from_none], obj.get("inferenceTransport")) + inference_type = from_union([ProviderType, from_none], obj.get("inferenceType")) + inference_wire_api = from_union([ProviderWireAPI, from_none], obj.get("inferenceWireApi")) + return ModelProviderInstance(display_name, provenance, reference, inference_endpoint, inference_transport, inference_type, inference_wire_api) + + def to_dict(self) -> dict: + result: dict = {} + result["displayName"] = from_str(self.display_name) + result["provenance"] = to_class(ModelProviderProvenance, self.provenance) + result["reference"] = to_class(ModelProviderInstanceReference, self.reference) + if self.inference_endpoint is not None: + result["inferenceEndpoint"] = from_union([from_str, from_none], self.inference_endpoint) + if self.inference_transport is not None: + result["inferenceTransport"] = from_union([lambda x: to_enum(ProviderTransport, x), from_none], self.inference_transport) + if self.inference_type is not None: + result["inferenceType"] = from_union([lambda x: to_enum(ProviderType, x), from_none], self.inference_type) + if self.inference_wire_api is not None: + result["inferenceWireApi"] = from_union([lambda x: to_enum(ProviderWireAPI, x), from_none], self.inference_wire_api) + return result + # Experimental: this type is part of an experimental API and may change or be removed. @dataclass class ExtensionList: @@ -34229,6 +34894,133 @@ def to_dict(self) -> dict: result["supports"] = from_union([lambda x: to_class(ModelCapabilitiesOverrideSupports, x), from_none], self.supports) return result +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class ModelProviderAdapterDescriptor: + """A normalized model-provider adapter in the session's effective catalog.""" + + adapter_id: str + """Stable opaque identity for routing to this adapter. Unique in the effective catalog, + independent of live registration generations. + """ + automatic_discovery: ModelProviderAutomaticDiscoveryPolicy + """Adapter-declared policy for passive and automatic discovery.""" + + display_name: str + """Human-readable provider name.""" + + operations: list[ModelProviderAdapterOperationDescriptor] + """Operations supported by this provider adapter.""" + + provenance: ModelProviderAttribution + """Attribution for the adapter itself.""" + + provider_kind: str + """Descriptive provider family, such as `ollama`. Different adapters may have the same + family; use adapterId for routing. + """ + + @staticmethod + def from_dict(obj: Any) -> 'ModelProviderAdapterDescriptor': + assert isinstance(obj, dict) + adapter_id = from_str(obj.get("adapterId")) + automatic_discovery = ModelProviderAutomaticDiscoveryPolicy.from_dict(obj.get("automaticDiscovery")) + display_name = from_str(obj.get("displayName")) + operations = from_list(ModelProviderAdapterOperationDescriptor.from_dict, obj.get("operations")) + provenance = ModelProviderAttribution.from_dict(obj.get("provenance")) + provider_kind = from_str(obj.get("providerKind")) + return ModelProviderAdapterDescriptor(adapter_id, automatic_discovery, display_name, operations, provenance, provider_kind) + + def to_dict(self) -> dict: + result: dict = {} + result["adapterId"] = from_str(self.adapter_id) + result["automaticDiscovery"] = to_class(ModelProviderAutomaticDiscoveryPolicy, self.automatic_discovery) + result["displayName"] = from_str(self.display_name) + result["operations"] = from_list(lambda x: to_class(ModelProviderAdapterOperationDescriptor, x), self.operations) + result["provenance"] = to_class(ModelProviderAttribution, self.provenance) + result["providerKind"] = from_str(self.provider_kind) + return result + +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class ProtocolSystemMessageConfig: + """System-message configuration used when the runtime builds the standard prompt for this + provider-qualified model, including general-purpose subagents. It uses the same object + hierarchy as session-level systemMessage configuration, except transform actions are + rejected because the current callback protocol is not model-scoped. When present, it + overrides the session-wide configuration on those prompt paths. Selected custom-agent and + specialized-subagent prompts remain authoritative. + """ + content: str | None = None + """Text appended to the standard system prompt. + + Complete replacement system-message text. + + Text appended after the customized sections. + """ + mode: ProtocolMode | None = None + """Append-mode discriminator. Omission also selects append mode. + + Replace-mode discriminator. + + Customize-mode discriminator. + """ + content_blocks: list[SystemMessageBlock] | None = None + """Optional structured blocks corresponding to the replacement content.""" + + sections: dict[str, ProtocolSectionOverride] | None = None + """Named standard-prompt section overrides.""" + + @staticmethod + def from_dict(obj: Any) -> 'ProtocolSystemMessageConfig': + assert isinstance(obj, dict) + content = from_union([from_str, from_none], obj.get("content")) + mode = from_union([ProtocolMode, from_none], obj.get("mode")) + content_blocks = from_union([lambda x: from_list(SystemMessageBlock.from_dict, x), from_none], obj.get("contentBlocks")) + sections = from_union([lambda x: from_dict(ProtocolSectionOverride.from_dict, x), from_none], obj.get("sections")) + return ProtocolSystemMessageConfig(content, mode, content_blocks, sections) + + def to_dict(self) -> dict: + result: dict = {} + if self.content is not None: + result["content"] = from_union([from_str, from_none], self.content) + if self.mode is not None: + result["mode"] = from_union([lambda x: to_enum(ProtocolMode, x), from_none], self.mode) + if self.content_blocks is not None: + result["contentBlocks"] = from_union([lambda x: from_list(lambda x: to_class(SystemMessageBlock, x), x), from_none], self.content_blocks) + if self.sections is not None: + result["sections"] = from_union([lambda x: from_dict(lambda x: to_class(ProtocolSectionOverride, x), x), from_none], self.sections) + return result + +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class ProtocolSystemMessageCustomizeConfig: + mode: ProtocolCustomizeMode + """Customize-mode discriminator.""" + + content: str | None = None + """Text appended after the customized sections.""" + + sections: dict[str, ProtocolSectionOverride] | None = None + """Named standard-prompt section overrides.""" + + @staticmethod + def from_dict(obj: Any) -> 'ProtocolSystemMessageCustomizeConfig': + assert isinstance(obj, dict) + mode = ProtocolCustomizeMode(obj.get("mode")) + content = from_union([from_str, from_none], obj.get("content")) + sections = from_union([lambda x: from_dict(ProtocolSectionOverride.from_dict, x), from_none], obj.get("sections")) + return ProtocolSystemMessageCustomizeConfig(mode, content, sections) + + def to_dict(self) -> dict: + result: dict = {} + result["mode"] = to_enum(ProtocolCustomizeMode, self.mode) + if self.content is not None: + result["content"] = from_union([from_str, from_none], self.content) + if self.sections is not None: + result["sections"] = from_union([lambda x: from_dict(lambda x: to_class(ProtocolSectionOverride, x), x), from_none], self.sections) + return result + # Experimental: this type is part of an experimental API and may change or be removed. @dataclass class OptionsUpdateAdditionalContentExclusionPolicy: @@ -34826,86 +35618,6 @@ def to_dict(self) -> dict: result["maxAiCredits"] = from_union([to_float, from_none], self.max_ai_credits) return result -# Experimental: this type is part of an experimental API and may change or be removed. -@dataclass -class ProtocolSystemMessageConfig: - """System-message configuration used when the runtime builds the standard prompt for this - provider-qualified model, including general-purpose subagents. It uses the same object - hierarchy as session-level systemMessage configuration, except transform actions are - rejected because the current callback protocol is not model-scoped. When present, it - overrides the session-wide configuration on those prompt paths. Selected custom-agent and - specialized-subagent prompts remain authoritative. - """ - content: str | None = None - """Text appended to the standard system prompt. - - Complete replacement system-message text. - - Text appended after the customized sections. - """ - mode: ProtocolMode | None = None - """Append-mode discriminator. Omission also selects append mode. - - Replace-mode discriminator. - - Customize-mode discriminator. - """ - content_blocks: list[SystemMessageBlock] | None = None - """Optional structured blocks corresponding to the replacement content.""" - - sections: dict[str, ProtocolSectionOverride] | None = None - """Named standard-prompt section overrides.""" - - @staticmethod - def from_dict(obj: Any) -> 'ProtocolSystemMessageConfig': - assert isinstance(obj, dict) - content = from_union([from_str, from_none], obj.get("content")) - mode = from_union([ProtocolMode, from_none], obj.get("mode")) - content_blocks = from_union([lambda x: from_list(SystemMessageBlock.from_dict, x), from_none], obj.get("contentBlocks")) - sections = from_union([lambda x: from_dict(ProtocolSectionOverride.from_dict, x), from_none], obj.get("sections")) - return ProtocolSystemMessageConfig(content, mode, content_blocks, sections) - - def to_dict(self) -> dict: - result: dict = {} - if self.content is not None: - result["content"] = from_union([from_str, from_none], self.content) - if self.mode is not None: - result["mode"] = from_union([lambda x: to_enum(ProtocolMode, x), from_none], self.mode) - if self.content_blocks is not None: - result["contentBlocks"] = from_union([lambda x: from_list(lambda x: to_class(SystemMessageBlock, x), x), from_none], self.content_blocks) - if self.sections is not None: - result["sections"] = from_union([lambda x: from_dict(lambda x: to_class(ProtocolSectionOverride, x), x), from_none], self.sections) - return result - -# Experimental: this type is part of an experimental API and may change or be removed. -@dataclass -class ProtocolSystemMessageCustomizeConfig: - mode: ProtocolCustomizeMode - """Customize-mode discriminator.""" - - content: str | None = None - """Text appended after the customized sections.""" - - sections: dict[str, ProtocolSectionOverride] | None = None - """Named standard-prompt section overrides.""" - - @staticmethod - def from_dict(obj: Any) -> 'ProtocolSystemMessageCustomizeConfig': - assert isinstance(obj, dict) - mode = ProtocolCustomizeMode(obj.get("mode")) - content = from_union([from_str, from_none], obj.get("content")) - sections = from_union([lambda x: from_dict(ProtocolSectionOverride.from_dict, x), from_none], obj.get("sections")) - return ProtocolSystemMessageCustomizeConfig(mode, content, sections) - - def to_dict(self) -> dict: - result: dict = {} - result["mode"] = to_enum(ProtocolCustomizeMode, self.mode) - if self.content is not None: - result["content"] = from_union([from_str, from_none], self.content) - if self.sections is not None: - result["sections"] = from_union([lambda x: from_dict(lambda x: to_class(ProtocolSectionOverride, x), x), from_none], self.sections) - return result - # Experimental: this type is part of an experimental API and may change or be removed. @dataclass class PushAttachmentGitHubFileDiff: @@ -35396,6 +36108,31 @@ def to_dict(self) -> dict: result["seatbelt"] = from_union([lambda x: to_class(SandboxConfigUserPolicySeatbelt, x), from_none], self.seatbelt) return result +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class SessionFSReadFileBytesResult: + """File bytes as standard base64, or a filesystem error if the read failed.""" + + content: str + """Exact file bytes encoded as standard base64""" + + error: SessionFSError | None = None + """Describes a filesystem error.""" + + @staticmethod + def from_dict(obj: Any) -> 'SessionFSReadFileBytesResult': + assert isinstance(obj, dict) + content = from_str(obj.get("content")) + error = from_union([SessionFSError.from_dict, from_none], obj.get("error")) + return SessionFSReadFileBytesResult(content, error) + + def to_dict(self) -> dict: + result: dict = {} + result["content"] = from_str(self.content) + if self.error is not None: + result["error"] = from_union([lambda x: to_class(SessionFSError, x), from_none], self.error) + return result + # Experimental: this type is part of an experimental API and may change or be removed. @dataclass class SessionFSReadFileResult: @@ -38399,6 +39136,68 @@ def to_dict(self) -> dict: result["sources"] = to_class(DiagnosticSourcesConfiguration, self.sources) return result +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class ModelProviderDiscoverResult: + """Provider instances found by a discovery operation.""" + + instances: list[ModelProviderInstance] + """Discovered provider instances. Empty when passive default discovery finds no reachable + provider. + """ + outcome: ModelProviderOperationOutcome + """Typed operation outcome. Passive discovery can return `absent` with an empty instance + list. + """ + + @staticmethod + def from_dict(obj: Any) -> 'ModelProviderDiscoverResult': + assert isinstance(obj, dict) + instances = from_list(ModelProviderInstance.from_dict, obj.get("instances")) + outcome = ModelProviderOperationOutcome.from_dict(obj.get("outcome")) + return ModelProviderDiscoverResult(instances, outcome) + + def to_dict(self) -> dict: + result: dict = {} + result["instances"] = from_list(lambda x: to_class(ModelProviderInstance, x), self.instances) + result["outcome"] = to_class(ModelProviderOperationOutcome, self.outcome) + return result + +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class ModelProviderStatus: + """Current health information for a provider instance.""" + + instance: ModelProviderInstance + """Normalized provider instance.""" + + outcome: ModelProviderOperationOutcome + """Typed operation outcome.""" + + status: str + """Open provider status value, such as `healthy`, `unreachable`, or `notInstalled`.""" + + version: str | None = None + """Provider-reported version.""" + + @staticmethod + def from_dict(obj: Any) -> 'ModelProviderStatus': + assert isinstance(obj, dict) + instance = ModelProviderInstance.from_dict(obj.get("instance")) + outcome = ModelProviderOperationOutcome.from_dict(obj.get("outcome")) + status = from_str(obj.get("status")) + version = from_union([from_str, from_none], obj.get("version")) + return ModelProviderStatus(instance, outcome, status, version) + + def to_dict(self) -> dict: + result: dict = {} + result["instance"] = to_class(ModelProviderInstance, self.instance) + result["outcome"] = to_class(ModelProviderOperationOutcome, self.outcome) + result["status"] = from_str(self.status) + if self.version is not None: + result["version"] = from_union([from_str, from_none], self.version) + return result + # Experimental: this type is part of an experimental API and may change or be removed. @dataclass class HandlePendingToolCallRequest: @@ -39296,6 +40095,26 @@ def to_dict(self) -> dict: result["wireModel"] = from_union([from_str, from_none], self.wire_model) return result +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class ModelProviderAdapterCatalog: + """Normalized model-provider adapter definitions available to the session, not discovered + instances. + """ + providers: list[ModelProviderAdapterDescriptor] + """Available provider adapters ordered by adapterId.""" + + @staticmethod + def from_dict(obj: Any) -> 'ModelProviderAdapterCatalog': + assert isinstance(obj, dict) + providers = from_list(ModelProviderAdapterDescriptor.from_dict, obj.get("providers")) + return ModelProviderAdapterCatalog(providers) + + def to_dict(self) -> dict: + result: dict = {} + result["providers"] = from_list(lambda x: to_class(ModelProviderAdapterDescriptor, x), self.providers) + return result + # Experimental: this type is part of an experimental API and may change or be removed. @dataclass class PermissionsConfigureParams: @@ -39681,8 +40500,11 @@ def to_dict(self) -> dict: # Experimental: this type is part of an experimental API and may change or be removed. @dataclass class SandboxConfig: - """Resolved sandbox configuration.""" + """Resolved sandbox configuration. + The sandbox configuration that the host gives its sessions. The runtime reads the + credential hosts from `auth` and `credentials`; it ignores `enabled` and the other fields. + """ enabled: bool """Whether sandboxing is enabled for the session.""" @@ -40882,6 +41704,30 @@ def to_dict(self) -> dict: result["servers"] = from_list(lambda x: to_class(DiscoveredMCPServer, x), self.servers) return result +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class SandboxProxyCARequest: + """Identifies the credential hosts that the persistent certificate authority of the sandbox + credential proxy must cover. The runtime always adds the hosts from the saved user + settings. + """ + sandbox_config: SandboxConfig | None = None + """The sandbox configuration that the host gives its sessions. The runtime reads the + credential hosts from `auth` and `credentials`; it ignores `enabled` and the other fields. + """ + + @staticmethod + def from_dict(obj: Any) -> 'SandboxProxyCARequest': + assert isinstance(obj, dict) + sandbox_config = from_union([SandboxConfig.from_dict, from_none], obj.get("sandboxConfig")) + return SandboxProxyCARequest(sandbox_config) + + def to_dict(self) -> dict: + result: dict = {} + if self.sandbox_config is not None: + result["sandboxConfig"] = from_union([lambda x: to_class(SandboxConfig, x), from_none], self.sandbox_config) + return result + # Experimental: this type is part of an experimental API and may change or be removed. @dataclass class SessionUpdateOptionsParams: @@ -42703,6 +43549,129 @@ def to_dict(self) -> dict: result["droppedCount"] = from_union([from_int, from_none], self.dropped_count) return result +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class ModelCapabilities: + """Provider-reported model capabilities. Omitted capability fields are unknown; explicit + false values are preserved. + + Model capabilities and limits + """ + limits: ModelCapabilitiesLimits | None = None + """Token limits for prompts, outputs, and context window""" + + supports: ModelCapabilitiesSupports | None = None + """Feature flags indicating what the model supports""" + + @staticmethod + def from_dict(obj: Any) -> 'ModelCapabilities': + assert isinstance(obj, dict) + limits = from_union([ModelCapabilitiesLimits.from_dict, from_none], obj.get("limits")) + supports = from_union([ModelCapabilitiesSupports.from_dict, from_none], obj.get("supports")) + return ModelCapabilities(limits, supports) + + def to_dict(self) -> dict: + result: dict = {} + if self.limits is not None: + result["limits"] = from_union([lambda x: to_class(ModelCapabilitiesLimits, x), from_none], self.limits) + if self.supports is not None: + result["supports"] = from_union([lambda x: to_class(ModelCapabilitiesSupports, x), from_none], self.supports) + return result + +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class DiscoveredModel: + """A model offered for agent conversations. Missing capability metadata does not disqualify + a candidate. Models known to be incompatible, such as embedding-only models, are excluded + by the adapter. + + The discovered model to configure. + """ + capabilities: ModelCapabilities + """Provider-reported model capabilities. Omitted capability fields are unknown; explicit + false values are preserved. + """ + details: ModelArtifactDetails + """Provider-reported model artifact details.""" + + id: str + """Provider-native model identifier.""" + + provenance: ModelProviderProvenance + """Attribution for the adapter that produced this model row.""" + + warnings: list[ModelProviderWarning] + """Non-fatal warnings encountered while enriching this model.""" + + digest: str | None = None + """Provider-reported artifact digest.""" + + modified_at: datetime | None = None + """Provider-reported last-modified timestamp.""" + + name: str | None = None + """Provider-reported display name.""" + + size_bytes: int | None = None + """Provider-reported artifact size in bytes.""" + + @staticmethod + def from_dict(obj: Any) -> 'DiscoveredModel': + assert isinstance(obj, dict) + capabilities = ModelCapabilities.from_dict(obj.get("capabilities")) + details = ModelArtifactDetails.from_dict(obj.get("details")) + id = from_str(obj.get("id")) + provenance = ModelProviderProvenance.from_dict(obj.get("provenance")) + warnings = from_list(ModelProviderWarning.from_dict, obj.get("warnings")) + digest = from_union([from_str, from_none], obj.get("digest")) + modified_at = from_union([from_datetime, from_none], obj.get("modifiedAt")) + name = from_union([from_str, from_none], obj.get("name")) + size_bytes = from_union([from_int, from_none], obj.get("sizeBytes")) + return DiscoveredModel(capabilities, details, id, provenance, warnings, digest, modified_at, name, size_bytes) + + def to_dict(self) -> dict: + result: dict = {} + result["capabilities"] = to_class(ModelCapabilities, self.capabilities) + result["details"] = to_class(ModelArtifactDetails, self.details) + result["id"] = from_str(self.id) + result["provenance"] = to_class(ModelProviderProvenance, self.provenance) + result["warnings"] = from_list(lambda x: to_class(ModelProviderWarning, x), self.warnings) + if self.digest is not None: + result["digest"] = from_union([from_str, from_none], self.digest) + if self.modified_at is not None: + result["modifiedAt"] = from_union([lambda x: x.isoformat(), from_none], self.modified_at) + if self.name is not None: + result["name"] = from_union([from_str, from_none], self.name) + if self.size_bytes is not None: + result["sizeBytes"] = from_union([from_int, from_none], self.size_bytes) + return result + +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class DiscoveredModelList: + """Models offered for agent conversations by one provider instance. Adapters exclude + known-incompatible models, but retain candidates with unknown capabilities. Listing does + not guarantee compatibility. + """ + models: list[DiscoveredModel] + """Provider-native models in provider order.""" + + outcome: ModelProviderOperationOutcome + """Typed operation outcome.""" + + @staticmethod + def from_dict(obj: Any) -> 'DiscoveredModelList': + assert isinstance(obj, dict) + models = from_list(DiscoveredModel.from_dict, obj.get("models")) + outcome = ModelProviderOperationOutcome.from_dict(obj.get("outcome")) + return DiscoveredModelList(models, outcome) + + def to_dict(self) -> dict: + result: dict = {} + result["models"] = from_list(lambda x: to_class(DiscoveredModel, x), self.models) + result["outcome"] = to_class(ModelProviderOperationOutcome, self.outcome) + return result + # Experimental: this type is part of an experimental API and may change or be removed. @dataclass class EnvAuthInfo: @@ -43927,32 +44896,6 @@ def to_dict(self) -> dict: result["modelId"] = from_str(self.model_id) return result -# Experimental: this type is part of an experimental API and may change or be removed. -@dataclass -class ModelCapabilities: - """Model capabilities and limits""" - - limits: ModelCapabilitiesLimits | None = None - """Token limits for prompts, outputs, and context window""" - - supports: ModelCapabilitiesSupports | None = None - """Feature flags indicating what the model supports""" - - @staticmethod - def from_dict(obj: Any) -> 'ModelCapabilities': - assert isinstance(obj, dict) - limits = from_union([ModelCapabilitiesLimits.from_dict, from_none], obj.get("limits")) - supports = from_union([ModelCapabilitiesSupports.from_dict, from_none], obj.get("supports")) - return ModelCapabilities(limits, supports) - - def to_dict(self) -> dict: - result: dict = {} - if self.limits is not None: - result["limits"] = from_union([lambda x: to_class(ModelCapabilitiesLimits, x), from_none], self.limits) - if self.supports is not None: - result["supports"] = from_union([lambda x: to_class(ModelCapabilitiesSupports, x), from_none], self.supports) - return result - # Experimental: this type is part of an experimental API and may change or be removed. class ModelPickerCategory(Enum): """Model capability category for grouping in the model picker""" @@ -44232,6 +45175,175 @@ def to_dict(self) -> dict: result["models"] = from_list(lambda x: to_class(Model, x), self.models) return result +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class ProviderModelConfig: + """Model definition prepared from the discovered model. Capability fields the provider did + not report stay omitted rather than being asserted false. + + A BYOK model definition referencing a named provider. + """ + id: str + """Provider-local model id, unique within its provider. The session-wide selection id (shown + in the model list and passed to switchTo) is the provider-qualified `provider/id`. + """ + provider: str + """Name of the configured provider that serves this model.""" + + capabilities: ModelCapabilitiesOverride | None = None + """Optional capability overrides (vision, tool_calls, reasoning, etc.).""" + + max_context_window_tokens: float | None = None + """Maximum context window tokens for the model.""" + + max_output_tokens: float | None = None + """Maximum output tokens for the model.""" + + max_prompt_tokens: float | None = None + """Maximum prompt/input tokens for the model.""" + + metadata: dict[str, Any] | None = None + """Provider-published model metadata, preserved verbatim as the public Model.metadata object.""" + + model_id: str | None = None + """Well-known base model id used for behavior/capability/config lookup. Defaults to `id`.""" + + name: str | None = None + """Display name for model pickers. Defaults to the provider-qualified selection id + (`provider/id`). + """ + system_message: ProtocolSystemMessageConfig | None = None + """System-message configuration used when the runtime builds the standard prompt for this + provider-qualified model, including general-purpose subagents. It uses the same object + hierarchy as session-level systemMessage configuration, except transform actions are + rejected because the current callback protocol is not model-scoped. When present, it + overrides the session-wide configuration on those prompt paths. Selected custom-agent and + specialized-subagent prompts remain authoritative. + """ + wire_model: str | None = None + """The model name sent to the provider API for inference. Defaults to `id`.""" + + @staticmethod + def from_dict(obj: Any) -> 'ProviderModelConfig': + assert isinstance(obj, dict) + id = from_str(obj.get("id")) + provider = from_str(obj.get("provider")) + capabilities = from_union([ModelCapabilitiesOverride.from_dict, from_none], obj.get("capabilities")) + max_context_window_tokens = from_union([from_float, from_none], obj.get("maxContextWindowTokens")) + max_output_tokens = from_union([from_float, from_none], obj.get("maxOutputTokens")) + max_prompt_tokens = from_union([from_float, from_none], obj.get("maxPromptTokens")) + metadata = from_union([lambda x: from_dict(lambda x: x, x), from_none], obj.get("metadata")) + model_id = from_union([from_str, from_none], obj.get("modelId")) + name = from_union([from_str, from_none], obj.get("name")) + system_message = from_union([ProtocolSystemMessageConfig.from_dict, from_none], obj.get("systemMessage")) + wire_model = from_union([from_str, from_none], obj.get("wireModel")) + return ProviderModelConfig(id, provider, capabilities, max_context_window_tokens, max_output_tokens, max_prompt_tokens, metadata, model_id, name, system_message, wire_model) + + def to_dict(self) -> dict: + result: dict = {} + result["id"] = from_str(self.id) + result["provider"] = from_str(self.provider) + if self.capabilities is not None: + result["capabilities"] = from_union([lambda x: to_class(ModelCapabilitiesOverride, x), from_none], self.capabilities) + if self.max_context_window_tokens is not None: + result["maxContextWindowTokens"] = from_union([to_float, from_none], self.max_context_window_tokens) + if self.max_output_tokens is not None: + result["maxOutputTokens"] = from_union([to_float, from_none], self.max_output_tokens) + if self.max_prompt_tokens is not None: + result["maxPromptTokens"] = from_union([to_float, from_none], self.max_prompt_tokens) + if self.metadata is not None: + result["metadata"] = from_union([lambda x: from_dict(lambda x: x, x), from_none], self.metadata) + if self.model_id is not None: + result["modelId"] = from_union([from_str, from_none], self.model_id) + if self.name is not None: + result["name"] = from_union([from_str, from_none], self.name) + if self.system_message is not None: + result["systemMessage"] = from_union([lambda x: to_class(ProtocolSystemMessageConfig, x), from_none], self.system_message) + if self.wire_model is not None: + result["wireModel"] = from_union([from_str, from_none], self.wire_model) + return result + +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class ModelProviderConfigurationPlan: + """Provider configuration prepared from a discovered model. Preparing a plan changes + nothing: it neither registers the model with the session nor writes durable + configuration. To apply it, pass `provider` and `model` to `session.provider.add`, + omitting whichever the dispositions report as already configured. + """ + model: ProviderModelConfig + """Model definition prepared from the discovered model. Capability fields the provider did + not report stay omitted rather than being asserted false. + """ + model_disposition: ModelProviderConfigurationDisposition + """Whether `model` still needs to be registered. When `alreadyConfigured`, `selectionId` is + already registered and the caller can select it without adding anything. + """ + provider: NamedProviderConfig + """Provider connection prepared from the instance's inference metadata. Carries no + credential; supply one if the endpoint requires it. + """ + provider_disposition: ModelProviderConfigurationDisposition + """Whether `provider` still needs to be registered. When `alreadyConfigured`, a provider + with the same endpoint is already registered and `provider` restates it under its + existing name; adding it again is rejected as a duplicate. + """ + selection_id: str + """Provider-qualified selection id (`provider/id`) to pass to `switchTo` once the plan is + applied. + """ + warnings: list[ModelProviderWarning] + """Non-fatal warnings carried over from the discovered model, such as capabilities the + provider did not report. + """ + + @staticmethod + def from_dict(obj: Any) -> 'ModelProviderConfigurationPlan': + assert isinstance(obj, dict) + model = ProviderModelConfig.from_dict(obj.get("model")) + model_disposition = ModelProviderConfigurationDisposition(obj.get("modelDisposition")) + provider = NamedProviderConfig.from_dict(obj.get("provider")) + provider_disposition = ModelProviderConfigurationDisposition(obj.get("providerDisposition")) + selection_id = from_str(obj.get("selectionId")) + warnings = from_list(ModelProviderWarning.from_dict, obj.get("warnings")) + return ModelProviderConfigurationPlan(model, model_disposition, provider, provider_disposition, selection_id, warnings) + + def to_dict(self) -> dict: + result: dict = {} + result["model"] = to_class(ProviderModelConfig, self.model) + result["modelDisposition"] = to_enum(ModelProviderConfigurationDisposition, self.model_disposition) + result["provider"] = to_class(NamedProviderConfig, self.provider) + result["providerDisposition"] = to_enum(ModelProviderConfigurationDisposition, self.provider_disposition) + result["selectionId"] = from_str(self.selection_id) + result["warnings"] = from_list(lambda x: to_class(ModelProviderWarning, x), self.warnings) + return result + +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class ModelProviderPrepareConfigurationRequest: + """A discovered instance and one of its models to translate into provider configuration. + Pass back the instance and model as returned by `session.providers.discover` and + `session.providers.models.list`. + """ + instance: ModelProviderInstance + """The discovered instance that serves the model.""" + + model: DiscoveredModel + """The discovered model to configure.""" + + @staticmethod + def from_dict(obj: Any) -> 'ModelProviderPrepareConfigurationRequest': + assert isinstance(obj, dict) + instance = ModelProviderInstance.from_dict(obj.get("instance")) + model = DiscoveredModel.from_dict(obj.get("model")) + return ModelProviderPrepareConfigurationRequest(instance, model) + + def to_dict(self) -> dict: + result: dict = {} + result["instance"] = to_class(ModelProviderInstance, self.instance) + result["model"] = to_class(DiscoveredModel, self.model) + return result + # Experimental: this type is part of an experimental API and may change or be removed. @dataclass class ModelSwitchToRequest: @@ -44501,91 +45613,6 @@ def to_dict(self) -> dict: result["title"] = from_union([from_str, from_none], self.title) return result -# Experimental: this type is part of an experimental API and may change or be removed. -@dataclass -class ProviderModelConfig: - """A BYOK model definition referencing a named provider.""" - - id: str - """Provider-local model id, unique within its provider. The session-wide selection id (shown - in the model list and passed to switchTo) is the provider-qualified `provider/id`. - """ - provider: str - """Name of the configured provider that serves this model.""" - - capabilities: ModelCapabilitiesOverride | None = None - """Optional capability overrides (vision, tool_calls, reasoning, etc.).""" - - max_context_window_tokens: float | None = None - """Maximum context window tokens for the model.""" - - max_output_tokens: float | None = None - """Maximum output tokens for the model.""" - - max_prompt_tokens: float | None = None - """Maximum prompt/input tokens for the model.""" - - metadata: dict[str, Any] | None = None - """Provider-published model metadata, preserved verbatim as the public Model.metadata object.""" - - model_id: str | None = None - """Well-known base model id used for behavior/capability/config lookup. Defaults to `id`.""" - - name: str | None = None - """Display name for model pickers. Defaults to the provider-qualified selection id - (`provider/id`). - """ - system_message: ProtocolSystemMessageConfig | None = None - """System-message configuration used when the runtime builds the standard prompt for this - provider-qualified model, including general-purpose subagents. It uses the same object - hierarchy as session-level systemMessage configuration, except transform actions are - rejected because the current callback protocol is not model-scoped. When present, it - overrides the session-wide configuration on those prompt paths. Selected custom-agent and - specialized-subagent prompts remain authoritative. - """ - wire_model: str | None = None - """The model name sent to the provider API for inference. Defaults to `id`.""" - - @staticmethod - def from_dict(obj: Any) -> 'ProviderModelConfig': - assert isinstance(obj, dict) - id = from_str(obj.get("id")) - provider = from_str(obj.get("provider")) - capabilities = from_union([ModelCapabilitiesOverride.from_dict, from_none], obj.get("capabilities")) - max_context_window_tokens = from_union([from_float, from_none], obj.get("maxContextWindowTokens")) - max_output_tokens = from_union([from_float, from_none], obj.get("maxOutputTokens")) - max_prompt_tokens = from_union([from_float, from_none], obj.get("maxPromptTokens")) - metadata = from_union([lambda x: from_dict(lambda x: x, x), from_none], obj.get("metadata")) - model_id = from_union([from_str, from_none], obj.get("modelId")) - name = from_union([from_str, from_none], obj.get("name")) - system_message = from_union([ProtocolSystemMessageConfig.from_dict, from_none], obj.get("systemMessage")) - wire_model = from_union([from_str, from_none], obj.get("wireModel")) - return ProviderModelConfig(id, provider, capabilities, max_context_window_tokens, max_output_tokens, max_prompt_tokens, metadata, model_id, name, system_message, wire_model) - - def to_dict(self) -> dict: - result: dict = {} - result["id"] = from_str(self.id) - result["provider"] = from_str(self.provider) - if self.capabilities is not None: - result["capabilities"] = from_union([lambda x: to_class(ModelCapabilitiesOverride, x), from_none], self.capabilities) - if self.max_context_window_tokens is not None: - result["maxContextWindowTokens"] = from_union([to_float, from_none], self.max_context_window_tokens) - if self.max_output_tokens is not None: - result["maxOutputTokens"] = from_union([to_float, from_none], self.max_output_tokens) - if self.max_prompt_tokens is not None: - result["maxPromptTokens"] = from_union([to_float, from_none], self.max_prompt_tokens) - if self.metadata is not None: - result["metadata"] = from_union([lambda x: from_dict(lambda x: x, x), from_none], self.metadata) - if self.model_id is not None: - result["modelId"] = from_union([from_str, from_none], self.model_id) - if self.name is not None: - result["name"] = from_union([from_str, from_none], self.name) - if self.system_message is not None: - result["systemMessage"] = from_union([lambda x: to_class(ProtocolSystemMessageConfig, x), from_none], self.system_message) - if self.wire_model is not None: - result["wireModel"] = from_union([from_str, from_none], self.wire_model) - return result - # Experimental: this type is part of an experimental API and may change or be removed. @dataclass class ProviderAddRequest: @@ -46648,6 +47675,10 @@ class RPC: copilot_user_response_quota_snapshots_premium_interactions: CopilotUserResponseQuotaSnapshotsPremiumInteractions current_model: CurrentModel current_tool_metadata: CurrentToolMetadata + customization_reload_outcome: CustomizationReloadOutcome + customization_reload_status: CustomizationReloadStatus + customization_reload_subsystem: CustomizationReloadSubsystem + customizations_reload_result: CustomizationsReloadResult debug_collect_logs_collected_entry: DebugCollectLogsCollectedEntry debug_collect_logs_destination: DebugCollectLogsDestination debug_collect_logs_entry: DebugCollectLogsEntry @@ -46680,11 +47711,10 @@ class RPC: discovered_hook: DiscoveredHook discovered_mcp_server: DiscoveredMCPServer discovered_mcp_server_type: DiscoveredMCPServerType + discovered_model: DiscoveredModel + discovered_model_list: DiscoveredModelList enqueue_command_params: EnqueueCommandParams enqueue_command_result: EnqueueCommandResult - entra_token_acquire_request: EntraTokenAcquireRequest - entra_token_acquire_result: EntraTokenAcquireResult - entra_token_interaction: EntraTokenInteraction env_auth_info: EnvAuthInfo environment_capabilities: EnvironmentCapabilities environment_kind: EnvironmentKind @@ -47077,6 +48107,7 @@ class RPC: metadata_update_client_metadata_request: MetadataUpdateClientMetadataRequest model: Model model_apply_startup_overlay_request: ModelApplyStartupOverlayRequest + model_artifact_details: ModelArtifactDetails model_billing: ModelBilling model_billing_promo: ModelBillingPromo model_billing_token_prices: ModelBillingTokenPrices @@ -47098,9 +48129,31 @@ class RPC: model_picker_settings_context: ModelPickerSettingsContext model_policy: ModelPolicy model_policy_state: ModelPolicyState + model_provider_adapter_catalog: ModelProviderAdapterCatalog + model_provider_adapter_descriptor: ModelProviderAdapterDescriptor + model_provider_adapter_operation_descriptor: ModelProviderAdapterOperationDescriptor + model_provider_attribution: ModelProviderAttribution + model_provider_automatic_discovery_mode: ModelProviderAutomaticDiscoveryMode + model_provider_automatic_discovery_policy: ModelProviderAutomaticDiscoveryPolicy + model_provider_configuration_disposition: ModelProviderConfigurationDisposition + model_provider_configuration_plan: ModelProviderConfigurationPlan model_provider_descriptor: ModelProviderDescriptor + model_provider_discover_request: ModelProviderDiscoverRequest + model_provider_discover_result: ModelProviderDiscoverResult + model_provider_discovery_network_scope: ModelProviderDiscoveryNetworkScope + model_provider_get_status_request: ModelProviderGetStatusRequest + model_provider_instance: ModelProviderInstance + model_provider_instance_reference: ModelProviderInstanceReference model_provider_kind: ModelProviderKind + model_provider_models_list_request: ModelProviderModelsListRequest + model_provider_operation_outcome: ModelProviderOperationOutcome + model_provider_operation_outcome_code: ModelProviderOperationOutcomeCode + model_provider_prepare_configuration_request: ModelProviderPrepareConfigurationRequest + model_provider_provenance: ModelProviderProvenance + model_provider_provenance_source: ModelProviderProvenanceSource model_provider_ref: ModelProviderRef + model_provider_status: ModelProviderStatus + model_provider_warning: ModelProviderWarning model_set_allowed_models_request: ModelSetAllowedModelsRequest model_set_allowed_models_result: ModelSetAllowedModelsResult model_set_reasoning_effort_request: ModelSetReasoningEffortRequest @@ -47401,6 +48454,10 @@ class RPC: sandbox_host_capability_name: str sandbox_host_support: SandboxHostSupport sandbox_masked_env_var: SandboxMaskedEnvVar + sandbox_proxy_ca_create_result: SandboxProxyCACreateResult + sandbox_proxy_ca_request: SandboxProxyCARequest + sandbox_proxy_ca_state: SandboxProxyCAState + sandbox_proxy_ca_status: SandboxProxyCAStatus sandbox_session_change: SandboxSessionChange schedule_add_at_request: ScheduleAddAtRequest schedule_add_cron_request: ScheduleAddCronRequest @@ -47454,6 +48511,8 @@ class RPC: session_fs_readdir_with_types_entry_type: DebugCollectLogsEntryKind session_fs_readdir_with_types_request: SessionFSReaddirWithTypesRequest session_fs_readdir_with_types_result: SessionFSReaddirWithTypesResult + session_fs_read_file_bytes_request: SessionFSReadFileBytesRequest + session_fs_read_file_bytes_result: SessionFSReadFileBytesResult session_fs_read_file_request: SessionFSReadFileRequest session_fs_read_file_result: SessionFSReadFileResult session_fs_rename_request: SessionFSRenameRequest @@ -47474,6 +48533,7 @@ class RPC: session_fs_sqlite_transaction_statement: SessionFSSqliteTransactionStatement session_fs_stat_request: SessionFSStatRequest session_fs_stat_result: SessionFSStatResult + session_fs_write_file_bytes_request: SessionFSWriteFileBytesRequest session_fs_write_file_request: SessionFSWriteFileRequest session_git_hub_auth_get_all_auth_available_result: list[SessionAuthStatus] session_git_hub_auth_logout_result: bool @@ -48146,6 +49206,10 @@ def from_dict(obj: Any) -> 'RPC': copilot_user_response_quota_snapshots_premium_interactions = CopilotUserResponseQuotaSnapshotsPremiumInteractions.from_dict(obj.get("CopilotUserResponseQuotaSnapshotsPremiumInteractions")) current_model = CurrentModel.from_dict(obj.get("CurrentModel")) current_tool_metadata = CurrentToolMetadata.from_dict(obj.get("CurrentToolMetadata")) + customization_reload_outcome = CustomizationReloadOutcome.from_dict(obj.get("CustomizationReloadOutcome")) + customization_reload_status = CustomizationReloadStatus(obj.get("CustomizationReloadStatus")) + customization_reload_subsystem = CustomizationReloadSubsystem(obj.get("CustomizationReloadSubsystem")) + customizations_reload_result = CustomizationsReloadResult.from_dict(obj.get("CustomizationsReloadResult")) debug_collect_logs_collected_entry = DebugCollectLogsCollectedEntry.from_dict(obj.get("DebugCollectLogsCollectedEntry")) debug_collect_logs_destination = DebugCollectLogsDestination.from_dict(obj.get("DebugCollectLogsDestination")) debug_collect_logs_entry = DebugCollectLogsEntry.from_dict(obj.get("DebugCollectLogsEntry")) @@ -48178,11 +49242,10 @@ def from_dict(obj: Any) -> 'RPC': discovered_hook = DiscoveredHook.from_dict(obj.get("DiscoveredHook")) discovered_mcp_server = DiscoveredMCPServer.from_dict(obj.get("DiscoveredMcpServer")) discovered_mcp_server_type = DiscoveredMCPServerType(obj.get("DiscoveredMcpServerType")) + discovered_model = DiscoveredModel.from_dict(obj.get("DiscoveredModel")) + discovered_model_list = DiscoveredModelList.from_dict(obj.get("DiscoveredModelList")) enqueue_command_params = EnqueueCommandParams.from_dict(obj.get("EnqueueCommandParams")) enqueue_command_result = _load_EnqueueCommandResult(obj.get("EnqueueCommandResult")) - entra_token_acquire_request = EntraTokenAcquireRequest.from_dict(obj.get("EntraTokenAcquireRequest")) - entra_token_acquire_result = EntraTokenAcquireResult.from_dict(obj.get("EntraTokenAcquireResult")) - entra_token_interaction = EntraTokenInteraction(obj.get("EntraTokenInteraction")) env_auth_info = EnvAuthInfo.from_dict(obj.get("EnvAuthInfo")) environment_capabilities = EnvironmentCapabilities.from_dict(obj.get("EnvironmentCapabilities")) environment_kind = EnvironmentKind(obj.get("EnvironmentKind")) @@ -48575,6 +49638,7 @@ def from_dict(obj: Any) -> 'RPC': metadata_update_client_metadata_request = MetadataUpdateClientMetadataRequest.from_dict(obj.get("MetadataUpdateClientMetadataRequest")) model = Model.from_dict(obj.get("Model")) model_apply_startup_overlay_request = ModelApplyStartupOverlayRequest.from_dict(obj.get("ModelApplyStartupOverlayRequest")) + model_artifact_details = ModelArtifactDetails.from_dict(obj.get("ModelArtifactDetails")) model_billing = ModelBilling.from_dict(obj.get("ModelBilling")) model_billing_promo = ModelBillingPromo.from_dict(obj.get("ModelBillingPromo")) model_billing_token_prices = ModelBillingTokenPrices.from_dict(obj.get("ModelBillingTokenPrices")) @@ -48596,9 +49660,31 @@ def from_dict(obj: Any) -> 'RPC': model_picker_settings_context = ModelPickerSettingsContext.from_dict(obj.get("ModelPickerSettingsContext")) model_policy = ModelPolicy.from_dict(obj.get("ModelPolicy")) model_policy_state = ModelPolicyState(obj.get("ModelPolicyState")) + model_provider_adapter_catalog = ModelProviderAdapterCatalog.from_dict(obj.get("ModelProviderAdapterCatalog")) + model_provider_adapter_descriptor = ModelProviderAdapterDescriptor.from_dict(obj.get("ModelProviderAdapterDescriptor")) + model_provider_adapter_operation_descriptor = ModelProviderAdapterOperationDescriptor.from_dict(obj.get("ModelProviderAdapterOperationDescriptor")) + model_provider_attribution = ModelProviderAttribution.from_dict(obj.get("ModelProviderAttribution")) + model_provider_automatic_discovery_mode = ModelProviderAutomaticDiscoveryMode(obj.get("ModelProviderAutomaticDiscoveryMode")) + model_provider_automatic_discovery_policy = ModelProviderAutomaticDiscoveryPolicy.from_dict(obj.get("ModelProviderAutomaticDiscoveryPolicy")) + model_provider_configuration_disposition = ModelProviderConfigurationDisposition(obj.get("ModelProviderConfigurationDisposition")) + model_provider_configuration_plan = ModelProviderConfigurationPlan.from_dict(obj.get("ModelProviderConfigurationPlan")) model_provider_descriptor = ModelProviderDescriptor.from_dict(obj.get("ModelProviderDescriptor")) + model_provider_discover_request = ModelProviderDiscoverRequest.from_dict(obj.get("ModelProviderDiscoverRequest")) + model_provider_discover_result = ModelProviderDiscoverResult.from_dict(obj.get("ModelProviderDiscoverResult")) + model_provider_discovery_network_scope = ModelProviderDiscoveryNetworkScope(obj.get("ModelProviderDiscoveryNetworkScope")) + model_provider_get_status_request = ModelProviderGetStatusRequest.from_dict(obj.get("ModelProviderGetStatusRequest")) + model_provider_instance = ModelProviderInstance.from_dict(obj.get("ModelProviderInstance")) + model_provider_instance_reference = ModelProviderInstanceReference.from_dict(obj.get("ModelProviderInstanceReference")) model_provider_kind = ModelProviderKind(obj.get("ModelProviderKind")) + model_provider_models_list_request = ModelProviderModelsListRequest.from_dict(obj.get("ModelProviderModelsListRequest")) + model_provider_operation_outcome = ModelProviderOperationOutcome.from_dict(obj.get("ModelProviderOperationOutcome")) + model_provider_operation_outcome_code = ModelProviderOperationOutcomeCode(obj.get("ModelProviderOperationOutcomeCode")) + model_provider_prepare_configuration_request = ModelProviderPrepareConfigurationRequest.from_dict(obj.get("ModelProviderPrepareConfigurationRequest")) + model_provider_provenance = ModelProviderProvenance.from_dict(obj.get("ModelProviderProvenance")) + model_provider_provenance_source = ModelProviderProvenanceSource(obj.get("ModelProviderProvenanceSource")) model_provider_ref = ModelProviderRef.from_dict(obj.get("ModelProviderRef")) + model_provider_status = ModelProviderStatus.from_dict(obj.get("ModelProviderStatus")) + model_provider_warning = ModelProviderWarning.from_dict(obj.get("ModelProviderWarning")) model_set_allowed_models_request = ModelSetAllowedModelsRequest.from_dict(obj.get("ModelSetAllowedModelsRequest")) model_set_allowed_models_result = ModelSetAllowedModelsResult.from_dict(obj.get("ModelSetAllowedModelsResult")) model_set_reasoning_effort_request = ModelSetReasoningEffortRequest.from_dict(obj.get("ModelSetReasoningEffortRequest")) @@ -48899,6 +49985,10 @@ def from_dict(obj: Any) -> 'RPC': sandbox_host_capability_name = from_str(obj.get("SandboxHostCapabilityName")) sandbox_host_support = SandboxHostSupport.from_dict(obj.get("SandboxHostSupport")) sandbox_masked_env_var = SandboxMaskedEnvVar.from_dict(obj.get("SandboxMaskedEnvVar")) + sandbox_proxy_ca_create_result = SandboxProxyCACreateResult.from_dict(obj.get("SandboxProxyCaCreateResult")) + sandbox_proxy_ca_request = SandboxProxyCARequest.from_dict(obj.get("SandboxProxyCaRequest")) + sandbox_proxy_ca_state = SandboxProxyCAState(obj.get("SandboxProxyCaState")) + sandbox_proxy_ca_status = SandboxProxyCAStatus.from_dict(obj.get("SandboxProxyCaStatus")) sandbox_session_change = SandboxSessionChange(obj.get("SandboxSessionChange")) schedule_add_at_request = ScheduleAddAtRequest.from_dict(obj.get("ScheduleAddAtRequest")) schedule_add_cron_request = ScheduleAddCronRequest.from_dict(obj.get("ScheduleAddCronRequest")) @@ -48952,6 +50042,8 @@ def from_dict(obj: Any) -> 'RPC': session_fs_readdir_with_types_entry_type = DebugCollectLogsEntryKind(obj.get("SessionFsReaddirWithTypesEntryType")) session_fs_readdir_with_types_request = SessionFSReaddirWithTypesRequest.from_dict(obj.get("SessionFsReaddirWithTypesRequest")) session_fs_readdir_with_types_result = SessionFSReaddirWithTypesResult.from_dict(obj.get("SessionFsReaddirWithTypesResult")) + session_fs_read_file_bytes_request = SessionFSReadFileBytesRequest.from_dict(obj.get("SessionFsReadFileBytesRequest")) + session_fs_read_file_bytes_result = SessionFSReadFileBytesResult.from_dict(obj.get("SessionFsReadFileBytesResult")) session_fs_read_file_request = SessionFSReadFileRequest.from_dict(obj.get("SessionFsReadFileRequest")) session_fs_read_file_result = SessionFSReadFileResult.from_dict(obj.get("SessionFsReadFileResult")) session_fs_rename_request = SessionFSRenameRequest.from_dict(obj.get("SessionFsRenameRequest")) @@ -48972,6 +50064,7 @@ def from_dict(obj: Any) -> 'RPC': session_fs_sqlite_transaction_statement = SessionFSSqliteTransactionStatement.from_dict(obj.get("SessionFsSqliteTransactionStatement")) session_fs_stat_request = SessionFSStatRequest.from_dict(obj.get("SessionFsStatRequest")) session_fs_stat_result = SessionFSStatResult.from_dict(obj.get("SessionFsStatResult")) + session_fs_write_file_bytes_request = SessionFSWriteFileBytesRequest.from_dict(obj.get("SessionFsWriteFileBytesRequest")) session_fs_write_file_request = SessionFSWriteFileRequest.from_dict(obj.get("SessionFsWriteFileRequest")) session_git_hub_auth_get_all_auth_available_result = from_list(SessionAuthStatus.from_dict, obj.get("SessionGitHubAuthGetAllAuthAvailableResult")) session_git_hub_auth_logout_result = from_bool(obj.get("SessionGitHubAuthLogoutResult")) @@ -49388,7 +50481,7 @@ def from_dict(obj: Any) -> 'RPC': subagent_settings = from_union([SubagentSettings.from_dict, from_none], obj.get("SubagentSettings")) task_progress = from_union([TaskProgress.from_dict, from_none], obj.get("TaskProgress")) workspace_summary = from_union([WorkspaceSummary.from_dict, from_none], obj.get("WorkspaceSummary")) - return RPC(abort_request, abort_result, accepted_enqueue_command_result, account_all_users, account_get_all_users_result, account_get_current_auth_result, account_get_quota_request, account_get_quota_result, account_kind, account_login_request, account_login_result, account_logout_request, account_logout_result, account_quota_snapshot, accounts_enumerate_request, accounts_get_request, accounts_set_request, account_status, adaptive_thinking_support, agent_discovery_path, agent_discovery_path_list, agent_discovery_path_scope, agent_get_current_result, agent_info, agent_info_source, agent_list, agent_list_request, agent_registry_live_target_entry, agent_registry_live_target_entry_attention_kind, agent_registry_live_target_entry_kind, agent_registry_live_target_entry_last_terminal_event, agent_registry_live_target_entry_status, agent_registry_log_capture, agent_registry_log_capture_open_error_reason, agent_registry_spawn_error, agent_registry_spawn_permission_mode, agent_registry_spawn_registry_timeout, agent_registry_spawn_request, agent_registry_spawn_result, agent_registry_spawn_spawned, agent_registry_spawn_validation_error, agent_registry_spawn_validation_error_field, agent_registry_spawn_validation_error_reason, agent_reload_result, agents_discover_request, agent_select_request, agent_select_result, agent_set_prompt_request, agents_get_discovery_paths_request, api_key_auth_info, auth_enumerate_query, auth_enumerate_value, auth_identity, auth_identity_metadata, auth_info, auth_info_type, auth_login_advance_request, auth_login_begin_request, auth_login_begun, auth_login_cancel_request, auth_login_result_dto, auth_login_result_status, auth_login_step, auth_read_query, auth_read_value, auth_status_dto, auth_validation_error, auth_validation_errors, auth_write, auth_write_result, autopilot_objective_credit_limit, autopilot_objective_get_state_result, autopilot_objective_state, autopilot_objective_status, built_in_model_catalog, built_in_model_catalog_entry, builtin_tool_descriptor, builtin_tool_format, builtin_tool_format_type, builtin_tool_input_schema, builtin_tool_input_schema_type, builtin_tool_safe_for_telemetry, builtin_tool_safe_telemetry_fields, cancel_user_requested_shell_command_result, canvas_action, canvas_action_invoke_request, canvas_action_invoke_result, canvas_close_request, canvas_host_context, canvas_host_context_capabilities, canvas_json_schema, canvas_list, canvas_list_open_result, canvas_open_request, canvas_provider_close_request, canvas_provider_invoke_action_request, canvas_provider_open_request, canvas_provider_open_result, canvas_provider_register_request, canvas_provider_unregister_request, canvas_session_context, capi_session_options, card_digest, card_digest_algorithm, card_digest_value, catalog_agent_plugin_candidate, catalog_agent_plugin_candidate_kind, catalog_agent_plugin_candidate_provenance, catalog_agent_plugin_compatibility_tag, catalog_agent_plugin_media_type, catalog_ai_skill_candidate, catalog_ai_skill_candidate_kind, catalog_ai_skill_candidate_provenance, catalog_ai_skill_installability, catalog_ai_skill_media_type, catalog_authentication_required_error, catalog_authentication_required_reason, catalog_candidate, catalog_candidate_kind, catalog_candidate_source, catalog_candidate_source_embedded, catalog_candidate_source_url, catalog_capability, catalog_capability_id, catalog_client_contract, catalog_contract_violation_error, catalog_contract_violation_reason, catalog_handle_rejected_error, catalog_handle_rejection_reason, catalog_handle_type, catalog_invalid_request_error, catalog_invalid_request_field, catalog_malformed_card_error, catalog_malformed_card_reason, catalog_mcp_server_candidate, catalog_mcp_server_candidate_kind, catalog_mcp_server_candidate_provenance, catalog_mcp_server_installability, catalog_media_type, catalog_negotiated_contract, catalog_negotiation_refused_error, catalog_negotiation_refused_reason, catalog_network_failure_error, catalog_network_failure_reason, catalog_not_installable_error, catalog_not_installable_reason, catalog_plugin_repository_source, catalog_policy_rejected_error, catalog_resource_identity, catalog_resource_version, catalog_search_page, catalog_search_pagination, catalog_search_request, catalog_search_result, catalog_search_succeeded, catalog_search_total_count_relation, catalog_selection_cancelled, catalog_selection_decision, catalog_selection_declined, catalog_selection_foreign, catalog_selection_invalid, catalog_selection_replayed, catalog_selection_request, catalog_selection_result, catalog_selection_selected, catalog_selection_stale, catalog_selection_timed_out, catalog_selection_wrong_kind, catalog_trust_eligibility, catalog_trust_provenance, catalog_trust_snapshot, catalog_trust_snapshot_absent, catalog_trust_snapshot_absent_status, catalog_trust_snapshot_current, catalog_trust_snapshot_current_status, catalog_trust_snapshot_downgraded, catalog_trust_snapshot_downgraded_status, catalog_trust_snapshot_malformed, catalog_trust_snapshot_malformed_status, catalog_trust_snapshot_revoked, catalog_trust_snapshot_revoked_status, catalog_trust_snapshot_schema_version, catalog_trust_snapshot_stale, catalog_trust_snapshot_stale_status, catalog_trust_snapshot_unsupported, catalog_trust_snapshot_unsupported_status, catalog_trust_source, catalog_trust_tier, catalog_unavailable_error, catalog_unavailable_reason, catalog_unavailable_transport_error, catalog_unavailable_transport_reason, catalog_unsafe_retrieval_error, catalog_unsafe_retrieval_reason, catalog_unsupported_kind_error, client_metadata, client_task_cancel_reason, client_task_cancel_request, client_task_cancel_result, command_list, commands_finalize_invocation_effect_request, commands_finalize_invocation_effect_result, commands_handle_pending_command_request, commands_handle_pending_command_result, commands_invocation_effect_outcome, commands_invocation_origin, commands_invoke_request, commands_list_request, commands_respond_to_queued_command_request, commands_respond_to_queued_command_result, completions_get_trigger_characters_result, completions_request_request, completions_request_result, configure_session_extensions_params, connect_client_info, connected_remote_session_metadata, connected_remote_session_metadata_kind, connected_remote_session_metadata_repository, connector_account_request, connector_authorization_requirement, connector_authorization_scope, connector_availability, connector_capabilities, connector_catalog_entry, connector_catalog_result, connector_catalog_status, connector_connect_request, connector_connect_result, connector_continue_request, connector_disconnect_result, connector_mcp_status, connector_reconcile_request, connector_runtime_status, connector_session_account, connector_status, connect_remote_session_params, connect_request, connect_result, content_exclusion_check_paths_request, content_exclusion_check_paths_result, content_exclusion_path_check, content_filter_mode, context_heaviest_message, copilot_api_token_auth_info, copilot_user_response, copilot_user_response_endpoints, copilot_user_response_quota_snapshots, copilot_user_response_quota_snapshots_chat, copilot_user_response_quota_snapshots_completions, copilot_user_response_quota_snapshots_premium_interactions, current_model, current_tool_metadata, debug_collect_logs_collected_entry, debug_collect_logs_destination, debug_collect_logs_entry, debug_collect_logs_entry_kind, debug_collect_logs_include, debug_collect_logs_redaction, debug_collect_logs_request, debug_collect_logs_result, debug_collect_logs_result_kind, debug_collect_logs_skipped_entry, debug_collect_logs_source, diagnostic_cursor_status, diagnostic_entry, diagnostic_log_level, diagnostics_configuration, diagnostics_configure_request, diagnostic_severity, diagnostic_source, diagnostic_sources_configuration, diagnostics_read_request, diagnostics_read_result, discovered_canvas, discovered_extension, discovered_extension_mode, discovered_extension_plugin, discovered_extensions, discovered_extensions_disable_request, discovered_extensions_enable_request, discovered_extension_source, discovered_hook, discovered_mcp_server, discovered_mcp_server_type, enqueue_command_params, enqueue_command_result, entra_token_acquire_request, entra_token_acquire_result, entra_token_interaction, env_auth_info, environment_capabilities, environment_kind, environments_delete_request, environments_delete_result, environments_get_request, environments_get_result, environments_list_request, environments_list_result, event_log_read_request, event_log_release_interest_result, event_log_tail_result, event_log_types, events_agent_scope, events_cursor_status, events_read_direction, events_read_result, execute_command_params, execute_command_result, extension, extension_context_push_input, extension_launch_profile, extension_launch_provider_resolve_request, extension_launch_provider_resolve_result, extension_list, extensions_disable_request, extensions_enable_request, extension_source, extension_status, external_tool_result, external_tool_text_result_for_llm, external_tool_text_result_for_llm_binary_results_for_llm, external_tool_text_result_for_llm_binary_results_for_llm_type, external_tool_text_result_for_llm_content, external_tool_text_result_for_llm_content_audio, external_tool_text_result_for_llm_content_image, external_tool_text_result_for_llm_content_resource, external_tool_text_result_for_llm_content_resource_details, external_tool_text_result_for_llm_content_resource_link, external_tool_text_result_for_llm_content_resource_link_icon, external_tool_text_result_for_llm_content_resource_link_icon_theme, external_tool_text_result_for_llm_content_shell_exit, external_tool_text_result_for_llm_content_terminal, external_tool_text_result_for_llm_content_text, filter_mapping, fleet_start_request, fleet_start_result, folder_trust_add_params, folder_trust_check_params, folder_trust_check_result, gh_cli_auth_info, git_hub_environment, git_hub_telemetry_client_info, git_hub_telemetry_event, git_hub_telemetry_notification, git_hub_token_acquire_reason, git_hub_token_acquire_request, git_hub_token_acquire_result, handle_pending_tool_call_request, handle_pending_tool_call_result, history_abort_manual_compaction_result, history_cancel_background_compaction_result, history_clear_context_request, history_clear_context_result, history_compact_context_window, history_compact_request, history_compact_result, history_file_restore_skip_reason, history_list_rewind_points_result, history_preview_rewind_request, history_preview_rewind_result, history_rewind_change_type, history_rewind_file_preview, history_rewind_mode, history_rewind_outcome, history_rewind_point, history_rewind_request, history_rewind_result, history_rewind_unavailable_reason, history_skipped_file_restore, history_summarize_for_handoff_result, history_truncate_request, history_truncate_result, hmac_auth_info, hook_invoke_request, hook_invoke_response, hook_origin, hooks_discover_request, hooks_discover_result, hook_type, host_configuration, host_dispose_request, host_empty_result, host_environment_credentials, host_exited_notification, host_exit_reason, host_git_hub_environment_options, host_local_server_configuration, host_local_server_options, host_publish_session_request, host_publish_session_result, host_ready_request, host_register_session_request, host_session_create_callback, host_session_create_request, host_session_create_result, host_session_released_notification, host_session_release_request, host_start_request, host_start_result, installation_catalogue_identity, installation_confirmation_request, installation_confirmation_response, installation_decision, installation_review, installed_plugin, installed_plugin_info, installed_plugin_source, installed_plugin_source_git_hub, installed_plugin_source_local, installed_plugin_source_url, instruction_discovery_path, instruction_discovery_path_kind, instruction_discovery_path_list, instruction_discovery_path_location, instructions_discover_request, instructions_get_discovery_paths_request, instructions_get_sources_result, instruction_source, instruction_source_location, instruction_source_type, interrupt_main_turn_request, interrupt_main_turn_result, json_schema_response_format, llm_inference_headers, llm_inference_http_request_chunk_request, llm_inference_http_request_chunk_result, llm_inference_http_request_start_request, llm_inference_http_request_start_result, llm_inference_http_request_start_transport, llm_inference_http_response_chunk_error, llm_inference_http_response_chunk_request, llm_inference_http_response_chunk_result, llm_inference_http_response_start_request, llm_inference_http_response_start_result, llm_inference_set_provider_result, local_session_metadata_value, login_provider_kind, log_request, log_result, lsp_initialize_request, managed_mcp_server_config, managed_setting_meta, managed_settings_channel, managed_settings_compose_layer, managed_settings_compose_request, managed_settings_compose_result, managed_settings_diagnostic, managed_settings_diagnostic_severity, managed_settings_layer, managed_settings_meta, managed_settings_read_result, managed_settings_resolved_data, managed_settings_resolve_request, managed_settings_resolve_result, managed_settings_schema_result, managed_settings_validate_request, managed_settings_validate_result, managed_settings_values, marketplace_add_result, marketplace_browse_result, marketplace_info, marketplace_list_result, marketplace_plugin_info, marketplace_refresh_entry, marketplace_refresh_result, marketplace_remove_result, mcp_allowed_server, mcp_apply_install_request, mcp_apply_uninstall_request, mcp_apps_call_tool_request, mcp_apps_diagnose_capability, mcp_apps_diagnose_request, mcp_apps_diagnose_result, mcp_apps_diagnose_server, mcp_apps_host_context, mcp_apps_host_context_details, mcp_apps_host_context_details_available_display_mode, mcp_apps_host_context_details_display_mode, mcp_apps_host_context_details_platform, mcp_apps_host_context_details_theme, mcp_apps_list_tools_request, mcp_apps_list_tools_result, mcp_apps_read_resource_request, mcp_apps_read_resource_result, mcp_apps_resource_content, mcp_apps_set_host_context_details, mcp_apps_set_host_context_details_available_display_mode, mcp_apps_set_host_context_details_display_mode, mcp_apps_set_host_context_details_platform, mcp_apps_set_host_context_details_theme, mcp_apps_set_host_context_request, mcp_cancel_sampling_execution_params, mcp_cancel_sampling_execution_result, mcp_config_add_request, mcp_config_disable_request, mcp_config_enable_request, mcp_config_list, mcp_config_remove_request, mcp_config_update_request, mcp_configure_git_hub_request, mcp_configure_git_hub_result, mcp_diagnostic_details, mcp_diagnostic_direction, mcp_diagnostic_kind, mcp_diagnostic_source_configuration, mcp_disable_request, mcp_discover_request, mcp_discover_result, mcp_elicitation_form_mode, mcp_enable_request, mcp_execute_sampling_params, mcp_execute_sampling_request, mcp_execute_sampling_result, mcp_failed_server, mcp_filtered_server, mcp_headers_handle_pending_headers_refresh_request, mcp_headers_handle_pending_headers_refresh_request_request, mcp_headers_handle_pending_headers_refresh_request_result, mcp_host_state, mcp_installation_failure_reason, mcp_installation_input, mcp_installation_management_outcome, mcp_installation_management_result, mcp_installation_operation_request, mcp_installation_operation_status, mcp_installation_outcome, mcp_installation_remote_configuration, mcp_installation_result, mcp_installation_review, mcp_installation_secret, mcp_installation_secret_storage, mcp_installations_request, mcp_installation_state, mcp_installation_summary, mcp_install_plan, mcp_is_server_running_request, mcp_is_server_running_result, mcp_list_tools_request, mcp_list_tools_result, mcp_oauth_authentication_state_changed_request, mcp_oauth_cancel_login_request, mcp_oauth_cancel_login_result, mcp_oauth_complete_request, mcp_oauth_handle_pending_request, mcp_oauth_handle_pending_result, mcp_oauth_login_grant_type, mcp_oauth_login_request, mcp_oauth_login_result, mcp_oauth_pending_request_response, mcp_oauth_prepare_login_request, mcp_oauth_prepare_login_result, mcp_oauth_probe_needs_auth_reason, mcp_oauth_probe_request, mcp_oauth_probe_result, mcp_oauth_respond_request, mcp_oauth_respond_result, mcp_owned_oauth_login_status, mcp_plan_configuration_change, mcp_plan_configuration_operation, mcp_plan_enum_value_type, mcp_plan_install_planned, mcp_plan_install_request, mcp_plan_install_result, mcp_plan_install_source, mcp_plan_install_source_candidate, mcp_plan_install_source_candidate_kind, mcp_plan_install_source_card, mcp_plan_install_source_card_kind, mcp_plan_package_install_method, mcp_plan_package_transport, mcp_plan_policy_decision, mcp_plan_policy_result, mcp_plan_policy_source, mcp_plan_provenance, mcp_plan_remote_install_method, mcp_plan_remote_transport, mcp_plan_required_value, mcp_plan_required_value_enum, mcp_plan_required_value_enum_kind, mcp_plan_required_value_scalar, mcp_plan_required_value_scalar_kind, mcp_plan_resource_identity, mcp_plan_scalar_value_type, mcp_plan_scope, mcp_plan_secret_placeholder, mcp_plan_secret_reference, mcp_plan_target, mcp_plan_transport_choice, mcp_plan_transport_choice_package, mcp_plan_transport_choice_remote, mcp_plan_uninstall_request, mcp_plan_value_category, mcp_prepared_install, mcp_prepare_install_request, mcp_prompt, mcp_prompt_argument, mcp_prompt_icon, mcp_prompt_message, mcp_prompt_role, mcp_prompts_get_request, mcp_prompts_get_result, mcp_prompts_list_request, mcp_prompts_list_result, mcp_register_external_client_request, mcp_reload_config, mcp_reload_with_config_request, mcp_remove_git_hub_result, mcp_resource, mcp_resource_annotations, mcp_resource_content, mcp_resource_icon, mcp_resources_list_request, mcp_resources_list_result, mcp_resources_list_templates_request, mcp_resources_list_templates_result, mcp_resources_read_request, mcp_resources_read_result, mcp_resource_template, mcp_restart_server_request, mcp_safe_for_telemetry, mcp_safe_for_telemetry_fields, mcp_sampling_execution_action, mcp_sampling_execution_result, mcp_serializable_server_config, mcp_server, mcp_server_auth_config, mcp_server_auth_config_redirect_port, mcp_server_card_embedded, mcp_server_card_embedded_kind, mcp_server_card_media_type, mcp_server_card_reference, mcp_server_card_url, mcp_server_card_url_kind, mcp_server_config, mcp_server_config_defer_tools, mcp_server_config_http, mcp_server_config_http_oauth_grant_type, mcp_server_config_http_type, mcp_server_config_memory, mcp_server_config_memory_type, mcp_server_config_stdio, mcp_server_config_stdio_type, mcp_server_failure_info, mcp_server_list, mcp_server_needs_auth_info, mcp_server_ownership, mcp_set_env_value_mode_details, mcp_set_env_value_mode_params, mcp_set_env_value_mode_result, mcp_source_file, mcp_source_plugin, mcp_source_ref, mcp_start_server_request, mcp_start_servers_result, mcp_stop_server_request, mcp_task_metadata, mcp_tools, mcp_tool_ui, mcp_tool_ui_visibility, mcp_uninstall_plan, mcp_unregister_external_client_request, memory_configuration, metadata_context_attribution_result, metadata_context_heaviest_messages_request, metadata_context_heaviest_messages_result, metadata_context_info_request, metadata_context_info_result, metadata_is_processing_result, metadata_recompute_context_tokens_request, metadata_recompute_context_tokens_result, metadata_record_context_change_request, metadata_record_context_change_result, metadata_set_working_directory_request, metadata_set_working_directory_result, metadata_snapshot_current_mode, metadata_snapshot_remote_metadata, metadata_snapshot_remote_metadata_repository, metadata_snapshot_remote_metadata_task_type, metadata_update_client_metadata_request, model, model_apply_startup_overlay_request, model_billing, model_billing_promo, model_billing_token_prices, model_billing_token_prices_long_context, model_capabilities, model_capabilities_limits, model_capabilities_limits_vision, model_capabilities_override, model_capabilities_override_limits, model_capabilities_override_limits_vision, model_capabilities_override_supports, model_capabilities_supports, model_list, model_list_request, model_message, model_picker_category, model_picker_persistence_request, model_picker_price_category, model_picker_settings_context, model_policy, model_policy_state, model_provider_descriptor, model_provider_kind, model_provider_ref, model_set_allowed_models_request, model_set_allowed_models_result, model_set_reasoning_effort_request, model_set_reasoning_effort_result, models_list_request, model_switch_auto_tier_request, model_switch_auto_tier_result, model_switch_auto_tier_status, model_switch_confirmation, model_switch_to_request, model_switch_to_result, model_warning_text, mode_set_request, mode_set_result, move_mcp_loading_to_background_result, named_provider_config, name_get_result, name_set_auto_request, name_set_auto_result, name_set_request, open_canvas_instance, options_update_additional_content_exclusion_policy, options_update_additional_content_exclusion_policy_rule, options_update_additional_content_exclusion_policy_rule_source, options_update_additional_content_exclusion_policy_scope, options_update_context_tier, options_update_env_value_mode, options_update_reasoning_summary, options_update_tool_filter_precedence, pending_permission_request, pending_permission_request_list, permission_decision, permission_decision_approved, permission_decision_approved_for_location, permission_decision_approved_for_session, permission_decision_approve_for_location, permission_decision_approve_for_location_approval, permission_decision_approve_for_location_approval_commands, permission_decision_approve_for_location_approval_custom_tool, permission_decision_approve_for_location_approval_extension_env_access, permission_decision_approve_for_location_approval_extension_management, permission_decision_approve_for_location_approval_extension_permission_access, permission_decision_approve_for_location_approval_mcp, permission_decision_approve_for_location_approval_mcp_sampling, permission_decision_approve_for_location_approval_memory, permission_decision_approve_for_location_approval_read, permission_decision_approve_for_location_approval_workflow, permission_decision_approve_for_location_approval_write, permission_decision_approve_for_session, permission_decision_approve_for_session_approval, permission_decision_approve_for_session_approval_commands, permission_decision_approve_for_session_approval_custom_tool, permission_decision_approve_for_session_approval_extension_env_access, permission_decision_approve_for_session_approval_extension_management, permission_decision_approve_for_session_approval_extension_permission_access, permission_decision_approve_for_session_approval_mcp, permission_decision_approve_for_session_approval_mcp_sampling, permission_decision_approve_for_session_approval_memory, permission_decision_approve_for_session_approval_read, permission_decision_approve_for_session_approval_workflow, permission_decision_approve_for_session_approval_write, permission_decision_approve_once, permission_decision_approve_permanently, permission_decision_approve_read_only_for_session, permission_decision_cancelled, permission_decision_context, permission_decision_denied_by_content_exclusion_policy, permission_decision_denied_by_permission_request_hook, permission_decision_denied_by_rules, permission_decision_denied_interactively_by_user, permission_decision_denied_no_approval_rule_and_could_not_request_from_user, permission_decision_outcome, permission_decision_reject, permission_decision_request, permission_decision_surface, permission_decision_user_not_available, permission_location_add_tool_approval_params, permission_location_apply_params, permission_location_apply_result, permission_location_resolve_params, permission_location_resolve_result, permission_location_type, permission_mode_source, permission_paths_add_params, permission_paths_allowed_check_params, permission_paths_allowed_check_result, permission_paths_config, permission_paths_list, permission_paths_update_primary_params, permission_paths_workspace_check_params, permission_paths_workspace_check_result, permission_prompt_shown_notification, permission_request_result, permission_response_capability, permission_rules_set, permissions_configure_additional_content_exclusion_policy, permissions_configure_additional_content_exclusion_policy_rule, permissions_configure_additional_content_exclusion_policy_rule_source, permissions_configure_additional_content_exclusion_policy_scope, permissions_configure_params, permissions_configure_result, permissions_folder_trust_add_trusted_result, permissions_get_mode_request, permissions_get_mode_result, permissions_locations_add_tool_approval_details, permissions_locations_add_tool_approval_details_commands, permissions_locations_add_tool_approval_details_custom_tool, permissions_locations_add_tool_approval_details_extension_env_access, permissions_locations_add_tool_approval_details_extension_management, permissions_locations_add_tool_approval_details_extension_permission_access, permissions_locations_add_tool_approval_details_mcp, permissions_locations_add_tool_approval_details_mcp_sampling, permissions_locations_add_tool_approval_details_memory, permissions_locations_add_tool_approval_details_read, permissions_locations_add_tool_approval_details_workflow, permissions_locations_add_tool_approval_details_write, permissions_locations_add_tool_approval_result, permissions_modify_rules_params, permissions_modify_rules_result, permissions_modify_rules_scope, permissions_notify_prompt_shown_result, permissions_paths_add_result, permissions_paths_list_request, permissions_paths_update_primary_result, permissions_pending_requests_request, permissions_reset_session_approvals_request, permissions_reset_session_approvals_result, permissions_set_approve_all_request, permissions_set_approve_all_result, permissions_set_approve_all_source, permissions_set_mode_request, permissions_set_mode_result, permissions_set_required_request, permissions_set_required_result, permissions_urls_set_unrestricted_mode_result, permission_urls_config, permission_urls_set_unrestricted_mode_params, ping_request, ping_result, plan_read_result, plan_read_sql_todos_result, plan_read_sql_todos_with_dependencies_result, plan_sql_todo_dependency, plan_sql_todos_row, plan_update_request, plugin, plugin_install_result, plugin_install_staging_mode, plugin_list, plugin_list_result, plugins_builtin_set_request, plugins_disable_request, plugins_enable_request, plugins_install_request, plugins_marketplaces_add_request, plugins_marketplaces_browse_request, plugins_marketplaces_refresh_request, plugins_marketplaces_remove_request, plugins_reload_request, plugins_uninstall_request, plugins_update_request, plugin_update_all_entry, plugin_update_all_result, plugin_update_result, protocol_append_mode, protocol_customize_mode, protocol_external_tool_defer, protocol_external_tool_definition, protocol_marker_section_override, protocol_replace_mode, protocol_section_override, protocol_static_section_action, protocol_static_section_override, protocol_system_message_append_config, protocol_system_message_config, protocol_system_message_customize_config, protocol_system_message_replace_config, provider_add_request, provider_add_result, provider_config, provider_config_azure, provider_config_model_provider, provider_config_transport, provider_config_type, provider_config_wire_api, provider_descriptor, provider_endpoint, provider_endpoint_transport, provider_endpoint_type, provider_endpoint_wire_api, provider_get_endpoint_request, provider_model_config, provider_session_token, provider_sync_request, provider_sync_result, provider_token_acquire_request, provider_token_acquire_result, provider_withdraw_request, provider_withdraw_result, push_attachment, push_attachment_blob, push_attachment_directory, push_attachment_file, push_attachment_file_line_range, push_attachment_git_hub_actions_job, push_attachment_git_hub_commit, push_attachment_git_hub_file, push_attachment_git_hub_file_diff, push_attachment_git_hub_file_diff_side, push_attachment_git_hub_reference, push_attachment_git_hub_reference_type, push_attachment_git_hub_release, push_attachment_git_hub_repository, push_attachment_git_hub_snippet, push_attachment_git_hub_tree_comparison, push_attachment_git_hub_tree_comparison_side, push_attachment_git_hub_url, push_attachment_selection, push_attachment_selection_details, push_attachment_selection_details_end, push_attachment_selection_details_start, push_git_hub_repo_ref, queue_append_steering_request, queue_begin_deferred_idle_drain_request, queue_begin_deferred_idle_drain_result, queue_consume_system_notifications_request, queued_command_handled, queued_command_not_handled, queued_command_result, queue_defer_session_idle_request, queue_duplicate_at_request, queue_duplicate_at_result, queue_enqueue_resume_pending_result, queue_finish_deferred_idle_drain_request, queue_finish_deferred_idle_drain_result, queue_has_pending_result, queue_insert_at_request, queue_insert_at_result, queue_insert_message, queue_move_item_request, queue_move_item_result, queue_pending_items, queue_pending_items_kind, queue_pending_items_result, queue_remove_at_request, queue_remove_at_result, queue_remove_most_recent_result, queue_send_now_request, queue_send_now_result, queue_set_drain_paused_request, queue_snapshot_result, queue_update_text_request, queue_update_text_result, queue_withdraw_message_request, queue_withdraw_message_result, register_event_interest_params, register_event_interest_result, release_event_interest_params, remote_control_config, remote_control_config_existing_mc_session, remote_control_status, remote_control_status_active, remote_control_status_connecting, remote_control_status_error, remote_control_status_off, remote_control_status_result, remote_control_stop_result, remote_control_transfer_result, remote_enable_request, remote_enable_result, remote_notify_steerable_changed_request, remote_notify_steerable_changed_result, remote_session_connection_result, remote_session_host_status, remote_session_metadata_repository, remote_session_metadata_task_type, remote_session_metadata_value, remote_session_mode, remote_session_repository, response_format, sandbox_config, sandbox_config_auth, sandbox_config_source, sandbox_config_user_policy, sandbox_config_user_policy_experimental, sandbox_config_user_policy_experimental_seatbelt, sandbox_config_user_policy_filesystem, sandbox_config_user_policy_network, sandbox_config_user_policy_network_proxy, sandbox_config_user_policy_seatbelt, sandbox_credentials_config, sandbox_disable_for_session_request, sandbox_disable_for_session_result, sandbox_enforcement_status, sandbox_grant_path_for_request_request, sandbox_grant_path_for_request_result, sandbox_host_capability, sandbox_host_capability_name, sandbox_host_support, sandbox_masked_env_var, sandbox_session_change, schedule_add_at_request, schedule_add_cron_request, schedule_add_request, schedule_add_result, schedule_add_self_paced_request, schedule_entry, schedule_has_self_paced_result, schedule_list, schedule_rearm_self_paced_request, schedule_stop_request, schedule_stop_result, secrets_add_filter_values_request, secrets_add_filter_values_result, send_agent_mode, send_attachments_to_message_params, send_message_item, send_messages_request, send_messages_result, send_mode, send_request, send_result, send_system_notification_request, server_agent_list, server_instruction_source_list, server_skill, server_skill_list, session_activity, session_agent_list_request, session_auth_login_request, session_auth_logout_user_request, session_auth_status, session_auth_switch_request, session_bulk_delete_result, session_cancel_all_background_agents_result, session_capability, session_commands_list_request, session_completion_item, session_context, session_context_host_type, session_enrich_metadata_result, session_fs_append_file_request, session_fs_error, session_fs_error_code, session_fs_exists_request, session_fs_exists_result, session_fs_mkdir_request, session_fs_readdir_request, session_fs_readdir_result, session_fs_readdir_with_types_entry, session_fs_readdir_with_types_entry_type, session_fs_readdir_with_types_request, session_fs_readdir_with_types_result, session_fs_read_file_request, session_fs_read_file_result, session_fs_rename_request, session_fs_rm_request, session_fs_set_provider_capabilities, session_fs_set_provider_conventions, session_fs_set_provider_request, session_fs_set_provider_result, session_fs_sqlite_exists_request, session_fs_sqlite_exists_result, session_fs_sqlite_query_request, session_fs_sqlite_query_result, session_fs_sqlite_query_type, session_fs_sqlite_transaction_error, session_fs_sqlite_transaction_error_class, session_fs_sqlite_transaction_request, session_fs_sqlite_transaction_result, session_fs_sqlite_transaction_statement, session_fs_stat_request, session_fs_stat_result, session_fs_write_file_request, session_git_hub_auth_get_all_auth_available_result, session_git_hub_auth_logout_result, session_git_hub_auth_logout_user_result, session_history_compact_request, session_installed_plugin, session_installed_plugin_source, session_installed_plugin_source_git_hub, session_installed_plugin_source_local, session_installed_plugin_source_url, session_limit_prediction_baseline_data, session_limit_prediction_client_type, session_limit_prediction_details, session_limit_prediction_predict_request, session_limit_prediction_request, session_limit_prediction_result, session_limit_prediction_source, session_limit_prediction_tier, session_limit_prediction_tier_option, session_limit_prediction_unavailable_reason, session_list, session_list_entry, session_list_filter, session_load_deferred_repo_hooks_result, session_log_level, session_managed_permissions, session_managed_settings, session_mcp_apps_call_tool_result, session_mcp_oauth_cancel_login_request, session_mcp_oauth_cancel_login_result, session_mcp_oauth_prepare_login_request, session_mcp_oauth_prepare_login_result, session_metadata_snapshot, session_mode, session_model_list, session_model_list_request, session_model_price_category, session_open_options, session_open_options_additional_content_exclusion_policy, session_open_options_additional_content_exclusion_policy_rule, session_open_options_additional_content_exclusion_policy_rule_source, session_open_options_additional_content_exclusion_policy_scope, session_open_options_env_value_mode, session_open_options_reasoning_summary, session_open_params, session_open_result, session_plugins_disable_request, session_plugins_enable_request, session_plugins_install_request, session_plugins_marketplaces_refresh_request, session_plugins_reload_request, session_provider_get_endpoint_request, session_prune_result, sessions_bulk_delete_request, sessions_check_in_use_request, sessions_check_in_use_result, sessions_client_metadata_entry, sessions_close_request, sessions_close_result, sessions_delete_request, sessions_enrich_metadata_request, session_set_credentials_params, session_set_credentials_result, session_settings_built_in_tool_availability_snapshot, session_settings_evaluate_predicate_request, session_settings_evaluate_predicate_result, session_settings_job_snapshot, session_settings_model_snapshot, session_settings_online_evaluation_snapshot, session_settings_predicate_name, session_settings_repo_snapshot, session_settings_snapshot, session_settings_validation_snapshot, sessions_find_by_prefix_request, sessions_find_by_prefix_result, sessions_find_by_task_id_request, sessions_find_by_task_id_result, sessions_fork_request, sessions_fork_result, sessions_get_board_entry_count_request, sessions_get_board_entry_count_result, sessions_get_client_metadata_request, sessions_get_client_metadata_result, sessions_get_event_file_path_request, sessions_get_event_file_path_result, sessions_get_last_for_context_request, sessions_get_last_for_context_result, sessions_get_metadata_request, sessions_get_metadata_result, sessions_get_persisted_remote_steerable_request, sessions_get_persisted_remote_steerable_result, session_sizes, sessions_list_non_empty_session_ids_request, sessions_list_non_empty_session_ids_result, sessions_list_request, sessions_load_deferred_repo_hooks_request, sessions_open_attach, sessions_open_cloud, sessions_open_create, sessions_open_handoff, sessions_open_handoff_task_type, sessions_open_progress, sessions_open_progress_status, sessions_open_progress_step, sessions_open_remote, sessions_open_resume, sessions_open_resume_last, sessions_open_status, session_source, sessions_prune_old_request, sessions_read_persisted_events_request, sessions_release_lock_request, sessions_release_lock_result, sessions_reload_plugin_hooks_request, sessions_reload_plugin_hooks_result, sessions_save_request, sessions_save_result, sessions_set_additional_plugins_request, sessions_set_additional_plugins_result, sessions_set_remote_control_steering_request, sessions_start_remote_control_request, sessions_stop_remote_control_request, sessions_transfer_remote_control_request, session_telemetry_engagement, session_update_options_params, session_update_options_result, session_visibility_status, session_workflow_pause_at_checkpoint_result, session_working_directory_context, session_working_directory_context_host_type, settable_auth_info, settable_token_auth_info, shell_cancel_user_requested_request, shell_credentials, shell_exec_request, shell_exec_result, shell_execute_user_requested_request, shell_init_profile, shell_init_script, shell_init_script_shell, shell_kill_request, shell_kill_result, shell_kill_signal, shell_options, shutdown_request, skill, skill_apply_install_request, skill_apply_uninstall_request, skill_discovery_path, skill_discovery_path_list, skill_discovery_scope, skill_installation_failure_reason, skill_installation_file_review, skill_installation_location, skill_installation_management_outcome, skill_installation_management_result, skill_installation_operation_request, skill_installation_operation_status, skill_installation_outcome, skill_installation_ownership_state, skill_installation_result, skill_installation_review, skill_installation_scope, skill_installation_session_state, skill_installation_source, skill_installations_request, skill_installation_summary, skill_install_plan, skill_list, skill_plan_install_request, skill_plan_uninstall_request, skill_provider_descriptor, skill_provider_list_request, skill_provider_list_result, skill_provider_read_request, skill_provider_read_result, skills_config_set_disabled_skills_request, skills_config_set_skill_disabled_request, skills_disable_request, skills_discover_request, skills_enable_request, skill_set_enabled_request, skills_get_discovery_paths_request, skills_get_invoked_result, skills_invoked_skill, skills_load_diagnostics, skill_uninstall_plan, slash_command_add_timeline_entry_result, slash_command_agent_prompt_result, slash_command_completed_result, slash_command_info, slash_command_input, slash_command_input_choice, slash_command_input_completion, slash_command_invocation_result, slash_command_kind, slash_command_model_picker_dialog, slash_command_select_subcommand_option, slash_command_select_subcommand_result, slash_command_set_model_result, slash_command_set_plan_model_result, slash_command_show_dialog_result, slash_command_text_result, slash_command_timeline_entry, subagent_settings_entry, subagent_settings_entry_context_tier, system_message_block, task_agent_info, task_agent_progress, task_client_active_status, task_client_execution_mode, task_client_info, task_client_owner, task_client_owner_kind, task_client_owner_presence, task_client_progress, task_client_status, task_client_type, task_client_update, task_complete_data, task_completion_decision, task_execution_mode, task_info, task_kind, task_list, task_progress_line, tasks_cancel_request, tasks_cancel_result, tasks_get_current_promotable_result, tasks_get_progress_request, tasks_get_progress_result, task_shell_info, task_shell_info_attachment_mode, task_shell_progress, tasks_promote_current_to_background_result, tasks_promote_to_background_request, tasks_promote_to_background_result, tasks_refresh_result, tasks_register_request, tasks_register_result, tasks_remove_request, tasks_remove_result, tasks_send_message_request, tasks_send_message_result, tasks_start_agent_request, tasks_start_agent_result, task_status, tasks_update_request, tasks_update_result, tasks_wait_for_pending_result, telemetry_set_feature_overrides_request, token_auth_info, token_provider_auth_info, tool, tool_list, tool_result, tool_result_expanded, tool_result_new_message, tool_result_type, tools_execute_request, tools_get_builtin_descriptors_request, tools_get_builtin_descriptors_result, tools_get_current_metadata_result, tools_initialize_and_validate_result, tools_list_request, tools_set_request, tools_set_result, tools_shell_descriptor_config, tools_task_complete_event_data_request, tools_update_subagent_settings_result, ui_auto_mode_switch_response, ui_elicitation_array_any_of_field, ui_elicitation_array_any_of_field_items, ui_elicitation_array_any_of_field_items_any_of, ui_elicitation_array_enum_field, ui_elicitation_array_enum_field_items, ui_elicitation_field_value, ui_elicitation_request, ui_elicitation_response, ui_elicitation_response_action, ui_elicitation_response_content, ui_elicitation_result, ui_elicitation_schema, ui_elicitation_schema_property, ui_elicitation_schema_property_boolean, ui_elicitation_schema_property_number, ui_elicitation_schema_property_number_type, ui_elicitation_schema_property_string, ui_elicitation_schema_property_string_format, ui_elicitation_string_enum_field, ui_elicitation_string_one_of_field, ui_elicitation_string_one_of_field_one_of, ui_ephemeral_query_request, ui_ephemeral_query_result, ui_exit_plan_mode_action, ui_exit_plan_mode_response, ui_handle_pending_auto_mode_switch_request, ui_handle_pending_elicitation_request, ui_handle_pending_exit_plan_mode_request, ui_handle_pending_result, ui_handle_pending_sampling_request, ui_handle_pending_sampling_response, ui_handle_pending_session_limits_exhausted_request, ui_handle_pending_user_input_request, ui_register_direct_auto_mode_switch_handler_result, ui_session_limits_exhausted_response, ui_session_limits_exhausted_response_action, ui_unregister_direct_auto_mode_switch_handler_request, ui_unregister_direct_auto_mode_switch_handler_result, ui_user_input_response, unsupported_enqueue_command_result, update_subagent_settings_request, usage_get_metrics_result, usage_metrics_agent_metric, usage_metrics_code_changes, usage_metrics_model_metric, usage_metrics_model_metric_requests, usage_metrics_model_metric_token_detail, usage_metrics_model_metric_usage, usage_metrics_token_detail, user_auth_info, user_requested_shell_command_result, user_setting_metadata, user_settings_get_result, user_settings_set_request, user_settings_set_result, visibility_get_result, visibility_set_request, visibility_set_result, workflow_abort_request, workflow_ack_result, workflow_agent_options, workflow_agent_request, workflow_agent_result, workflow_agent_summary, workflow_cancel_request, workflow_current_phase, workflow_declared_limits, workflow_durable_operation, workflow_execute_request, workflow_execute_result, workflow_get_run_progress_request, workflow_get_run_request, workflow_journal_get_request, workflow_journal_get_result, workflow_journal_put_request, workflow_list_runs_request, workflow_list_runs_result, workflow_log_line, workflow_log_line_kind, workflow_log_request, workflow_pause_checkpoint_action, workflow_pause_checkpoint_request, workflow_pause_checkpoint_result, workflow_pause_info, workflow_pause_request, workflow_phase_observation, workflow_phase_status, workflow_progress_line, workflow_progress_page, workflow_resume_request, workflow_resume_result, workflow_run_consumed, workflow_run_detail, workflow_run_failure, workflow_run_failure_kind, workflow_run_limits, workflow_run_options, workflow_run_request, workflow_run_result, workflow_run_status, workflow_run_summary, workflow_run_terminal, workflow_tool_resume_request, workflow_tool_run_options, workflow_tool_run_request, workspace_diff_file_change, workspace_diff_file_change_type, workspace_diff_mode, workspace_diff_result, workspaces_add_summary_request, workspaces_add_summary_result, workspaces_autopilot_objective_exists_result, workspaces_checkpoints, workspaces_create_directory_request, workspaces_create_file_request, workspaces_delete_autopilot_objective_result, workspaces_diff_request, workspaces_ensure_request, workspaces_get_workspace_result, workspaces_list_checkpoints_result, workspaces_list_files_result, workspaces_read_autopilot_objective_result, workspaces_read_checkpoint_request, workspaces_read_checkpoint_result, workspaces_read_file_request, workspaces_read_file_result, workspaces_remove_path_request, workspaces_rename_path_request, workspaces_save_large_paste_request, workspaces_save_large_paste_result, workspaces_stat_file_request, workspaces_stat_file_result, workspaces_truncate_summaries_request, workspace_summary_host_type, workspaces_update_metadata_request, workspaces_workspace_details_host_type, workspaces_write_autopilot_objective_request, workspaces_write_autopilot_objective_result, connector_session_account_result, session_auth_info_result, session_context_attribution, session_context_info, subagent_settings, task_progress, workspace_summary) + return RPC(abort_request, abort_result, accepted_enqueue_command_result, account_all_users, account_get_all_users_result, account_get_current_auth_result, account_get_quota_request, account_get_quota_result, account_kind, account_login_request, account_login_result, account_logout_request, account_logout_result, account_quota_snapshot, accounts_enumerate_request, accounts_get_request, accounts_set_request, account_status, adaptive_thinking_support, agent_discovery_path, agent_discovery_path_list, agent_discovery_path_scope, agent_get_current_result, agent_info, agent_info_source, agent_list, agent_list_request, agent_registry_live_target_entry, agent_registry_live_target_entry_attention_kind, agent_registry_live_target_entry_kind, agent_registry_live_target_entry_last_terminal_event, agent_registry_live_target_entry_status, agent_registry_log_capture, agent_registry_log_capture_open_error_reason, agent_registry_spawn_error, agent_registry_spawn_permission_mode, agent_registry_spawn_registry_timeout, agent_registry_spawn_request, agent_registry_spawn_result, agent_registry_spawn_spawned, agent_registry_spawn_validation_error, agent_registry_spawn_validation_error_field, agent_registry_spawn_validation_error_reason, agent_reload_result, agents_discover_request, agent_select_request, agent_select_result, agent_set_prompt_request, agents_get_discovery_paths_request, api_key_auth_info, auth_enumerate_query, auth_enumerate_value, auth_identity, auth_identity_metadata, auth_info, auth_info_type, auth_login_advance_request, auth_login_begin_request, auth_login_begun, auth_login_cancel_request, auth_login_result_dto, auth_login_result_status, auth_login_step, auth_read_query, auth_read_value, auth_status_dto, auth_validation_error, auth_validation_errors, auth_write, auth_write_result, autopilot_objective_credit_limit, autopilot_objective_get_state_result, autopilot_objective_state, autopilot_objective_status, built_in_model_catalog, built_in_model_catalog_entry, builtin_tool_descriptor, builtin_tool_format, builtin_tool_format_type, builtin_tool_input_schema, builtin_tool_input_schema_type, builtin_tool_safe_for_telemetry, builtin_tool_safe_telemetry_fields, cancel_user_requested_shell_command_result, canvas_action, canvas_action_invoke_request, canvas_action_invoke_result, canvas_close_request, canvas_host_context, canvas_host_context_capabilities, canvas_json_schema, canvas_list, canvas_list_open_result, canvas_open_request, canvas_provider_close_request, canvas_provider_invoke_action_request, canvas_provider_open_request, canvas_provider_open_result, canvas_provider_register_request, canvas_provider_unregister_request, canvas_session_context, capi_session_options, card_digest, card_digest_algorithm, card_digest_value, catalog_agent_plugin_candidate, catalog_agent_plugin_candidate_kind, catalog_agent_plugin_candidate_provenance, catalog_agent_plugin_compatibility_tag, catalog_agent_plugin_media_type, catalog_ai_skill_candidate, catalog_ai_skill_candidate_kind, catalog_ai_skill_candidate_provenance, catalog_ai_skill_installability, catalog_ai_skill_media_type, catalog_authentication_required_error, catalog_authentication_required_reason, catalog_candidate, catalog_candidate_kind, catalog_candidate_source, catalog_candidate_source_embedded, catalog_candidate_source_url, catalog_capability, catalog_capability_id, catalog_client_contract, catalog_contract_violation_error, catalog_contract_violation_reason, catalog_handle_rejected_error, catalog_handle_rejection_reason, catalog_handle_type, catalog_invalid_request_error, catalog_invalid_request_field, catalog_malformed_card_error, catalog_malformed_card_reason, catalog_mcp_server_candidate, catalog_mcp_server_candidate_kind, catalog_mcp_server_candidate_provenance, catalog_mcp_server_installability, catalog_media_type, catalog_negotiated_contract, catalog_negotiation_refused_error, catalog_negotiation_refused_reason, catalog_network_failure_error, catalog_network_failure_reason, catalog_not_installable_error, catalog_not_installable_reason, catalog_plugin_repository_source, catalog_policy_rejected_error, catalog_resource_identity, catalog_resource_version, catalog_search_page, catalog_search_pagination, catalog_search_request, catalog_search_result, catalog_search_succeeded, catalog_search_total_count_relation, catalog_selection_cancelled, catalog_selection_decision, catalog_selection_declined, catalog_selection_foreign, catalog_selection_invalid, catalog_selection_replayed, catalog_selection_request, catalog_selection_result, catalog_selection_selected, catalog_selection_stale, catalog_selection_timed_out, catalog_selection_wrong_kind, catalog_trust_eligibility, catalog_trust_provenance, catalog_trust_snapshot, catalog_trust_snapshot_absent, catalog_trust_snapshot_absent_status, catalog_trust_snapshot_current, catalog_trust_snapshot_current_status, catalog_trust_snapshot_downgraded, catalog_trust_snapshot_downgraded_status, catalog_trust_snapshot_malformed, catalog_trust_snapshot_malformed_status, catalog_trust_snapshot_revoked, catalog_trust_snapshot_revoked_status, catalog_trust_snapshot_schema_version, catalog_trust_snapshot_stale, catalog_trust_snapshot_stale_status, catalog_trust_snapshot_unsupported, catalog_trust_snapshot_unsupported_status, catalog_trust_source, catalog_trust_tier, catalog_unavailable_error, catalog_unavailable_reason, catalog_unavailable_transport_error, catalog_unavailable_transport_reason, catalog_unsafe_retrieval_error, catalog_unsafe_retrieval_reason, catalog_unsupported_kind_error, client_metadata, client_task_cancel_reason, client_task_cancel_request, client_task_cancel_result, command_list, commands_finalize_invocation_effect_request, commands_finalize_invocation_effect_result, commands_handle_pending_command_request, commands_handle_pending_command_result, commands_invocation_effect_outcome, commands_invocation_origin, commands_invoke_request, commands_list_request, commands_respond_to_queued_command_request, commands_respond_to_queued_command_result, completions_get_trigger_characters_result, completions_request_request, completions_request_result, configure_session_extensions_params, connect_client_info, connected_remote_session_metadata, connected_remote_session_metadata_kind, connected_remote_session_metadata_repository, connector_account_request, connector_authorization_requirement, connector_authorization_scope, connector_availability, connector_capabilities, connector_catalog_entry, connector_catalog_result, connector_catalog_status, connector_connect_request, connector_connect_result, connector_continue_request, connector_disconnect_result, connector_mcp_status, connector_reconcile_request, connector_runtime_status, connector_session_account, connector_status, connect_remote_session_params, connect_request, connect_result, content_exclusion_check_paths_request, content_exclusion_check_paths_result, content_exclusion_path_check, content_filter_mode, context_heaviest_message, copilot_api_token_auth_info, copilot_user_response, copilot_user_response_endpoints, copilot_user_response_quota_snapshots, copilot_user_response_quota_snapshots_chat, copilot_user_response_quota_snapshots_completions, copilot_user_response_quota_snapshots_premium_interactions, current_model, current_tool_metadata, customization_reload_outcome, customization_reload_status, customization_reload_subsystem, customizations_reload_result, debug_collect_logs_collected_entry, debug_collect_logs_destination, debug_collect_logs_entry, debug_collect_logs_entry_kind, debug_collect_logs_include, debug_collect_logs_redaction, debug_collect_logs_request, debug_collect_logs_result, debug_collect_logs_result_kind, debug_collect_logs_skipped_entry, debug_collect_logs_source, diagnostic_cursor_status, diagnostic_entry, diagnostic_log_level, diagnostics_configuration, diagnostics_configure_request, diagnostic_severity, diagnostic_source, diagnostic_sources_configuration, diagnostics_read_request, diagnostics_read_result, discovered_canvas, discovered_extension, discovered_extension_mode, discovered_extension_plugin, discovered_extensions, discovered_extensions_disable_request, discovered_extensions_enable_request, discovered_extension_source, discovered_hook, discovered_mcp_server, discovered_mcp_server_type, discovered_model, discovered_model_list, enqueue_command_params, enqueue_command_result, env_auth_info, environment_capabilities, environment_kind, environments_delete_request, environments_delete_result, environments_get_request, environments_get_result, environments_list_request, environments_list_result, event_log_read_request, event_log_release_interest_result, event_log_tail_result, event_log_types, events_agent_scope, events_cursor_status, events_read_direction, events_read_result, execute_command_params, execute_command_result, extension, extension_context_push_input, extension_launch_profile, extension_launch_provider_resolve_request, extension_launch_provider_resolve_result, extension_list, extensions_disable_request, extensions_enable_request, extension_source, extension_status, external_tool_result, external_tool_text_result_for_llm, external_tool_text_result_for_llm_binary_results_for_llm, external_tool_text_result_for_llm_binary_results_for_llm_type, external_tool_text_result_for_llm_content, external_tool_text_result_for_llm_content_audio, external_tool_text_result_for_llm_content_image, external_tool_text_result_for_llm_content_resource, external_tool_text_result_for_llm_content_resource_details, external_tool_text_result_for_llm_content_resource_link, external_tool_text_result_for_llm_content_resource_link_icon, external_tool_text_result_for_llm_content_resource_link_icon_theme, external_tool_text_result_for_llm_content_shell_exit, external_tool_text_result_for_llm_content_terminal, external_tool_text_result_for_llm_content_text, filter_mapping, fleet_start_request, fleet_start_result, folder_trust_add_params, folder_trust_check_params, folder_trust_check_result, gh_cli_auth_info, git_hub_environment, git_hub_telemetry_client_info, git_hub_telemetry_event, git_hub_telemetry_notification, git_hub_token_acquire_reason, git_hub_token_acquire_request, git_hub_token_acquire_result, handle_pending_tool_call_request, handle_pending_tool_call_result, history_abort_manual_compaction_result, history_cancel_background_compaction_result, history_clear_context_request, history_clear_context_result, history_compact_context_window, history_compact_request, history_compact_result, history_file_restore_skip_reason, history_list_rewind_points_result, history_preview_rewind_request, history_preview_rewind_result, history_rewind_change_type, history_rewind_file_preview, history_rewind_mode, history_rewind_outcome, history_rewind_point, history_rewind_request, history_rewind_result, history_rewind_unavailable_reason, history_skipped_file_restore, history_summarize_for_handoff_result, history_truncate_request, history_truncate_result, hmac_auth_info, hook_invoke_request, hook_invoke_response, hook_origin, hooks_discover_request, hooks_discover_result, hook_type, host_configuration, host_dispose_request, host_empty_result, host_environment_credentials, host_exited_notification, host_exit_reason, host_git_hub_environment_options, host_local_server_configuration, host_local_server_options, host_publish_session_request, host_publish_session_result, host_ready_request, host_register_session_request, host_session_create_callback, host_session_create_request, host_session_create_result, host_session_released_notification, host_session_release_request, host_start_request, host_start_result, installation_catalogue_identity, installation_confirmation_request, installation_confirmation_response, installation_decision, installation_review, installed_plugin, installed_plugin_info, installed_plugin_source, installed_plugin_source_git_hub, installed_plugin_source_local, installed_plugin_source_url, instruction_discovery_path, instruction_discovery_path_kind, instruction_discovery_path_list, instruction_discovery_path_location, instructions_discover_request, instructions_get_discovery_paths_request, instructions_get_sources_result, instruction_source, instruction_source_location, instruction_source_type, interrupt_main_turn_request, interrupt_main_turn_result, json_schema_response_format, llm_inference_headers, llm_inference_http_request_chunk_request, llm_inference_http_request_chunk_result, llm_inference_http_request_start_request, llm_inference_http_request_start_result, llm_inference_http_request_start_transport, llm_inference_http_response_chunk_error, llm_inference_http_response_chunk_request, llm_inference_http_response_chunk_result, llm_inference_http_response_start_request, llm_inference_http_response_start_result, llm_inference_set_provider_result, local_session_metadata_value, login_provider_kind, log_request, log_result, lsp_initialize_request, managed_mcp_server_config, managed_setting_meta, managed_settings_channel, managed_settings_compose_layer, managed_settings_compose_request, managed_settings_compose_result, managed_settings_diagnostic, managed_settings_diagnostic_severity, managed_settings_layer, managed_settings_meta, managed_settings_read_result, managed_settings_resolved_data, managed_settings_resolve_request, managed_settings_resolve_result, managed_settings_schema_result, managed_settings_validate_request, managed_settings_validate_result, managed_settings_values, marketplace_add_result, marketplace_browse_result, marketplace_info, marketplace_list_result, marketplace_plugin_info, marketplace_refresh_entry, marketplace_refresh_result, marketplace_remove_result, mcp_allowed_server, mcp_apply_install_request, mcp_apply_uninstall_request, mcp_apps_call_tool_request, mcp_apps_diagnose_capability, mcp_apps_diagnose_request, mcp_apps_diagnose_result, mcp_apps_diagnose_server, mcp_apps_host_context, mcp_apps_host_context_details, mcp_apps_host_context_details_available_display_mode, mcp_apps_host_context_details_display_mode, mcp_apps_host_context_details_platform, mcp_apps_host_context_details_theme, mcp_apps_list_tools_request, mcp_apps_list_tools_result, mcp_apps_read_resource_request, mcp_apps_read_resource_result, mcp_apps_resource_content, mcp_apps_set_host_context_details, mcp_apps_set_host_context_details_available_display_mode, mcp_apps_set_host_context_details_display_mode, mcp_apps_set_host_context_details_platform, mcp_apps_set_host_context_details_theme, mcp_apps_set_host_context_request, mcp_cancel_sampling_execution_params, mcp_cancel_sampling_execution_result, mcp_config_add_request, mcp_config_disable_request, mcp_config_enable_request, mcp_config_list, mcp_config_remove_request, mcp_config_update_request, mcp_configure_git_hub_request, mcp_configure_git_hub_result, mcp_diagnostic_details, mcp_diagnostic_direction, mcp_diagnostic_kind, mcp_diagnostic_source_configuration, mcp_disable_request, mcp_discover_request, mcp_discover_result, mcp_elicitation_form_mode, mcp_enable_request, mcp_execute_sampling_params, mcp_execute_sampling_request, mcp_execute_sampling_result, mcp_failed_server, mcp_filtered_server, mcp_headers_handle_pending_headers_refresh_request, mcp_headers_handle_pending_headers_refresh_request_request, mcp_headers_handle_pending_headers_refresh_request_result, mcp_host_state, mcp_installation_failure_reason, mcp_installation_input, mcp_installation_management_outcome, mcp_installation_management_result, mcp_installation_operation_request, mcp_installation_operation_status, mcp_installation_outcome, mcp_installation_remote_configuration, mcp_installation_result, mcp_installation_review, mcp_installation_secret, mcp_installation_secret_storage, mcp_installations_request, mcp_installation_state, mcp_installation_summary, mcp_install_plan, mcp_is_server_running_request, mcp_is_server_running_result, mcp_list_tools_request, mcp_list_tools_result, mcp_oauth_authentication_state_changed_request, mcp_oauth_cancel_login_request, mcp_oauth_cancel_login_result, mcp_oauth_complete_request, mcp_oauth_handle_pending_request, mcp_oauth_handle_pending_result, mcp_oauth_login_grant_type, mcp_oauth_login_request, mcp_oauth_login_result, mcp_oauth_pending_request_response, mcp_oauth_prepare_login_request, mcp_oauth_prepare_login_result, mcp_oauth_probe_needs_auth_reason, mcp_oauth_probe_request, mcp_oauth_probe_result, mcp_oauth_respond_request, mcp_oauth_respond_result, mcp_owned_oauth_login_status, mcp_plan_configuration_change, mcp_plan_configuration_operation, mcp_plan_enum_value_type, mcp_plan_install_planned, mcp_plan_install_request, mcp_plan_install_result, mcp_plan_install_source, mcp_plan_install_source_candidate, mcp_plan_install_source_candidate_kind, mcp_plan_install_source_card, mcp_plan_install_source_card_kind, mcp_plan_package_install_method, mcp_plan_package_transport, mcp_plan_policy_decision, mcp_plan_policy_result, mcp_plan_policy_source, mcp_plan_provenance, mcp_plan_remote_install_method, mcp_plan_remote_transport, mcp_plan_required_value, mcp_plan_required_value_enum, mcp_plan_required_value_enum_kind, mcp_plan_required_value_scalar, mcp_plan_required_value_scalar_kind, mcp_plan_resource_identity, mcp_plan_scalar_value_type, mcp_plan_scope, mcp_plan_secret_placeholder, mcp_plan_secret_reference, mcp_plan_target, mcp_plan_transport_choice, mcp_plan_transport_choice_package, mcp_plan_transport_choice_remote, mcp_plan_uninstall_request, mcp_plan_value_category, mcp_prepared_install, mcp_prepare_install_request, mcp_prompt, mcp_prompt_argument, mcp_prompt_icon, mcp_prompt_message, mcp_prompt_role, mcp_prompts_get_request, mcp_prompts_get_result, mcp_prompts_list_request, mcp_prompts_list_result, mcp_register_external_client_request, mcp_reload_config, mcp_reload_with_config_request, mcp_remove_git_hub_result, mcp_resource, mcp_resource_annotations, mcp_resource_content, mcp_resource_icon, mcp_resources_list_request, mcp_resources_list_result, mcp_resources_list_templates_request, mcp_resources_list_templates_result, mcp_resources_read_request, mcp_resources_read_result, mcp_resource_template, mcp_restart_server_request, mcp_safe_for_telemetry, mcp_safe_for_telemetry_fields, mcp_sampling_execution_action, mcp_sampling_execution_result, mcp_serializable_server_config, mcp_server, mcp_server_auth_config, mcp_server_auth_config_redirect_port, mcp_server_card_embedded, mcp_server_card_embedded_kind, mcp_server_card_media_type, mcp_server_card_reference, mcp_server_card_url, mcp_server_card_url_kind, mcp_server_config, mcp_server_config_defer_tools, mcp_server_config_http, mcp_server_config_http_oauth_grant_type, mcp_server_config_http_type, mcp_server_config_memory, mcp_server_config_memory_type, mcp_server_config_stdio, mcp_server_config_stdio_type, mcp_server_failure_info, mcp_server_list, mcp_server_needs_auth_info, mcp_server_ownership, mcp_set_env_value_mode_details, mcp_set_env_value_mode_params, mcp_set_env_value_mode_result, mcp_source_file, mcp_source_plugin, mcp_source_ref, mcp_start_server_request, mcp_start_servers_result, mcp_stop_server_request, mcp_task_metadata, mcp_tools, mcp_tool_ui, mcp_tool_ui_visibility, mcp_uninstall_plan, mcp_unregister_external_client_request, memory_configuration, metadata_context_attribution_result, metadata_context_heaviest_messages_request, metadata_context_heaviest_messages_result, metadata_context_info_request, metadata_context_info_result, metadata_is_processing_result, metadata_recompute_context_tokens_request, metadata_recompute_context_tokens_result, metadata_record_context_change_request, metadata_record_context_change_result, metadata_set_working_directory_request, metadata_set_working_directory_result, metadata_snapshot_current_mode, metadata_snapshot_remote_metadata, metadata_snapshot_remote_metadata_repository, metadata_snapshot_remote_metadata_task_type, metadata_update_client_metadata_request, model, model_apply_startup_overlay_request, model_artifact_details, model_billing, model_billing_promo, model_billing_token_prices, model_billing_token_prices_long_context, model_capabilities, model_capabilities_limits, model_capabilities_limits_vision, model_capabilities_override, model_capabilities_override_limits, model_capabilities_override_limits_vision, model_capabilities_override_supports, model_capabilities_supports, model_list, model_list_request, model_message, model_picker_category, model_picker_persistence_request, model_picker_price_category, model_picker_settings_context, model_policy, model_policy_state, model_provider_adapter_catalog, model_provider_adapter_descriptor, model_provider_adapter_operation_descriptor, model_provider_attribution, model_provider_automatic_discovery_mode, model_provider_automatic_discovery_policy, model_provider_configuration_disposition, model_provider_configuration_plan, model_provider_descriptor, model_provider_discover_request, model_provider_discover_result, model_provider_discovery_network_scope, model_provider_get_status_request, model_provider_instance, model_provider_instance_reference, model_provider_kind, model_provider_models_list_request, model_provider_operation_outcome, model_provider_operation_outcome_code, model_provider_prepare_configuration_request, model_provider_provenance, model_provider_provenance_source, model_provider_ref, model_provider_status, model_provider_warning, model_set_allowed_models_request, model_set_allowed_models_result, model_set_reasoning_effort_request, model_set_reasoning_effort_result, models_list_request, model_switch_auto_tier_request, model_switch_auto_tier_result, model_switch_auto_tier_status, model_switch_confirmation, model_switch_to_request, model_switch_to_result, model_warning_text, mode_set_request, mode_set_result, move_mcp_loading_to_background_result, named_provider_config, name_get_result, name_set_auto_request, name_set_auto_result, name_set_request, open_canvas_instance, options_update_additional_content_exclusion_policy, options_update_additional_content_exclusion_policy_rule, options_update_additional_content_exclusion_policy_rule_source, options_update_additional_content_exclusion_policy_scope, options_update_context_tier, options_update_env_value_mode, options_update_reasoning_summary, options_update_tool_filter_precedence, pending_permission_request, pending_permission_request_list, permission_decision, permission_decision_approved, permission_decision_approved_for_location, permission_decision_approved_for_session, permission_decision_approve_for_location, permission_decision_approve_for_location_approval, permission_decision_approve_for_location_approval_commands, permission_decision_approve_for_location_approval_custom_tool, permission_decision_approve_for_location_approval_extension_env_access, permission_decision_approve_for_location_approval_extension_management, permission_decision_approve_for_location_approval_extension_permission_access, permission_decision_approve_for_location_approval_mcp, permission_decision_approve_for_location_approval_mcp_sampling, permission_decision_approve_for_location_approval_memory, permission_decision_approve_for_location_approval_read, permission_decision_approve_for_location_approval_workflow, permission_decision_approve_for_location_approval_write, permission_decision_approve_for_session, permission_decision_approve_for_session_approval, permission_decision_approve_for_session_approval_commands, permission_decision_approve_for_session_approval_custom_tool, permission_decision_approve_for_session_approval_extension_env_access, permission_decision_approve_for_session_approval_extension_management, permission_decision_approve_for_session_approval_extension_permission_access, permission_decision_approve_for_session_approval_mcp, permission_decision_approve_for_session_approval_mcp_sampling, permission_decision_approve_for_session_approval_memory, permission_decision_approve_for_session_approval_read, permission_decision_approve_for_session_approval_workflow, permission_decision_approve_for_session_approval_write, permission_decision_approve_once, permission_decision_approve_permanently, permission_decision_approve_read_only_for_session, permission_decision_cancelled, permission_decision_context, permission_decision_denied_by_content_exclusion_policy, permission_decision_denied_by_permission_request_hook, permission_decision_denied_by_rules, permission_decision_denied_interactively_by_user, permission_decision_denied_no_approval_rule_and_could_not_request_from_user, permission_decision_outcome, permission_decision_reject, permission_decision_request, permission_decision_surface, permission_decision_user_not_available, permission_location_add_tool_approval_params, permission_location_apply_params, permission_location_apply_result, permission_location_resolve_params, permission_location_resolve_result, permission_location_type, permission_mode_source, permission_paths_add_params, permission_paths_allowed_check_params, permission_paths_allowed_check_result, permission_paths_config, permission_paths_list, permission_paths_update_primary_params, permission_paths_workspace_check_params, permission_paths_workspace_check_result, permission_prompt_shown_notification, permission_request_result, permission_response_capability, permission_rules_set, permissions_configure_additional_content_exclusion_policy, permissions_configure_additional_content_exclusion_policy_rule, permissions_configure_additional_content_exclusion_policy_rule_source, permissions_configure_additional_content_exclusion_policy_scope, permissions_configure_params, permissions_configure_result, permissions_folder_trust_add_trusted_result, permissions_get_mode_request, permissions_get_mode_result, permissions_locations_add_tool_approval_details, permissions_locations_add_tool_approval_details_commands, permissions_locations_add_tool_approval_details_custom_tool, permissions_locations_add_tool_approval_details_extension_env_access, permissions_locations_add_tool_approval_details_extension_management, permissions_locations_add_tool_approval_details_extension_permission_access, permissions_locations_add_tool_approval_details_mcp, permissions_locations_add_tool_approval_details_mcp_sampling, permissions_locations_add_tool_approval_details_memory, permissions_locations_add_tool_approval_details_read, permissions_locations_add_tool_approval_details_workflow, permissions_locations_add_tool_approval_details_write, permissions_locations_add_tool_approval_result, permissions_modify_rules_params, permissions_modify_rules_result, permissions_modify_rules_scope, permissions_notify_prompt_shown_result, permissions_paths_add_result, permissions_paths_list_request, permissions_paths_update_primary_result, permissions_pending_requests_request, permissions_reset_session_approvals_request, permissions_reset_session_approvals_result, permissions_set_approve_all_request, permissions_set_approve_all_result, permissions_set_approve_all_source, permissions_set_mode_request, permissions_set_mode_result, permissions_set_required_request, permissions_set_required_result, permissions_urls_set_unrestricted_mode_result, permission_urls_config, permission_urls_set_unrestricted_mode_params, ping_request, ping_result, plan_read_result, plan_read_sql_todos_result, plan_read_sql_todos_with_dependencies_result, plan_sql_todo_dependency, plan_sql_todos_row, plan_update_request, plugin, plugin_install_result, plugin_install_staging_mode, plugin_list, plugin_list_result, plugins_builtin_set_request, plugins_disable_request, plugins_enable_request, plugins_install_request, plugins_marketplaces_add_request, plugins_marketplaces_browse_request, plugins_marketplaces_refresh_request, plugins_marketplaces_remove_request, plugins_reload_request, plugins_uninstall_request, plugins_update_request, plugin_update_all_entry, plugin_update_all_result, plugin_update_result, protocol_append_mode, protocol_customize_mode, protocol_external_tool_defer, protocol_external_tool_definition, protocol_marker_section_override, protocol_replace_mode, protocol_section_override, protocol_static_section_action, protocol_static_section_override, protocol_system_message_append_config, protocol_system_message_config, protocol_system_message_customize_config, protocol_system_message_replace_config, provider_add_request, provider_add_result, provider_config, provider_config_azure, provider_config_model_provider, provider_config_transport, provider_config_type, provider_config_wire_api, provider_descriptor, provider_endpoint, provider_endpoint_transport, provider_endpoint_type, provider_endpoint_wire_api, provider_get_endpoint_request, provider_model_config, provider_session_token, provider_sync_request, provider_sync_result, provider_token_acquire_request, provider_token_acquire_result, provider_withdraw_request, provider_withdraw_result, push_attachment, push_attachment_blob, push_attachment_directory, push_attachment_file, push_attachment_file_line_range, push_attachment_git_hub_actions_job, push_attachment_git_hub_commit, push_attachment_git_hub_file, push_attachment_git_hub_file_diff, push_attachment_git_hub_file_diff_side, push_attachment_git_hub_reference, push_attachment_git_hub_reference_type, push_attachment_git_hub_release, push_attachment_git_hub_repository, push_attachment_git_hub_snippet, push_attachment_git_hub_tree_comparison, push_attachment_git_hub_tree_comparison_side, push_attachment_git_hub_url, push_attachment_selection, push_attachment_selection_details, push_attachment_selection_details_end, push_attachment_selection_details_start, push_git_hub_repo_ref, queue_append_steering_request, queue_begin_deferred_idle_drain_request, queue_begin_deferred_idle_drain_result, queue_consume_system_notifications_request, queued_command_handled, queued_command_not_handled, queued_command_result, queue_defer_session_idle_request, queue_duplicate_at_request, queue_duplicate_at_result, queue_enqueue_resume_pending_result, queue_finish_deferred_idle_drain_request, queue_finish_deferred_idle_drain_result, queue_has_pending_result, queue_insert_at_request, queue_insert_at_result, queue_insert_message, queue_move_item_request, queue_move_item_result, queue_pending_items, queue_pending_items_kind, queue_pending_items_result, queue_remove_at_request, queue_remove_at_result, queue_remove_most_recent_result, queue_send_now_request, queue_send_now_result, queue_set_drain_paused_request, queue_snapshot_result, queue_update_text_request, queue_update_text_result, queue_withdraw_message_request, queue_withdraw_message_result, register_event_interest_params, register_event_interest_result, release_event_interest_params, remote_control_config, remote_control_config_existing_mc_session, remote_control_status, remote_control_status_active, remote_control_status_connecting, remote_control_status_error, remote_control_status_off, remote_control_status_result, remote_control_stop_result, remote_control_transfer_result, remote_enable_request, remote_enable_result, remote_notify_steerable_changed_request, remote_notify_steerable_changed_result, remote_session_connection_result, remote_session_host_status, remote_session_metadata_repository, remote_session_metadata_task_type, remote_session_metadata_value, remote_session_mode, remote_session_repository, response_format, sandbox_config, sandbox_config_auth, sandbox_config_source, sandbox_config_user_policy, sandbox_config_user_policy_experimental, sandbox_config_user_policy_experimental_seatbelt, sandbox_config_user_policy_filesystem, sandbox_config_user_policy_network, sandbox_config_user_policy_network_proxy, sandbox_config_user_policy_seatbelt, sandbox_credentials_config, sandbox_disable_for_session_request, sandbox_disable_for_session_result, sandbox_enforcement_status, sandbox_grant_path_for_request_request, sandbox_grant_path_for_request_result, sandbox_host_capability, sandbox_host_capability_name, sandbox_host_support, sandbox_masked_env_var, sandbox_proxy_ca_create_result, sandbox_proxy_ca_request, sandbox_proxy_ca_state, sandbox_proxy_ca_status, sandbox_session_change, schedule_add_at_request, schedule_add_cron_request, schedule_add_request, schedule_add_result, schedule_add_self_paced_request, schedule_entry, schedule_has_self_paced_result, schedule_list, schedule_rearm_self_paced_request, schedule_stop_request, schedule_stop_result, secrets_add_filter_values_request, secrets_add_filter_values_result, send_agent_mode, send_attachments_to_message_params, send_message_item, send_messages_request, send_messages_result, send_mode, send_request, send_result, send_system_notification_request, server_agent_list, server_instruction_source_list, server_skill, server_skill_list, session_activity, session_agent_list_request, session_auth_login_request, session_auth_logout_user_request, session_auth_status, session_auth_switch_request, session_bulk_delete_result, session_cancel_all_background_agents_result, session_capability, session_commands_list_request, session_completion_item, session_context, session_context_host_type, session_enrich_metadata_result, session_fs_append_file_request, session_fs_error, session_fs_error_code, session_fs_exists_request, session_fs_exists_result, session_fs_mkdir_request, session_fs_readdir_request, session_fs_readdir_result, session_fs_readdir_with_types_entry, session_fs_readdir_with_types_entry_type, session_fs_readdir_with_types_request, session_fs_readdir_with_types_result, session_fs_read_file_bytes_request, session_fs_read_file_bytes_result, session_fs_read_file_request, session_fs_read_file_result, session_fs_rename_request, session_fs_rm_request, session_fs_set_provider_capabilities, session_fs_set_provider_conventions, session_fs_set_provider_request, session_fs_set_provider_result, session_fs_sqlite_exists_request, session_fs_sqlite_exists_result, session_fs_sqlite_query_request, session_fs_sqlite_query_result, session_fs_sqlite_query_type, session_fs_sqlite_transaction_error, session_fs_sqlite_transaction_error_class, session_fs_sqlite_transaction_request, session_fs_sqlite_transaction_result, session_fs_sqlite_transaction_statement, session_fs_stat_request, session_fs_stat_result, session_fs_write_file_bytes_request, session_fs_write_file_request, session_git_hub_auth_get_all_auth_available_result, session_git_hub_auth_logout_result, session_git_hub_auth_logout_user_result, session_history_compact_request, session_installed_plugin, session_installed_plugin_source, session_installed_plugin_source_git_hub, session_installed_plugin_source_local, session_installed_plugin_source_url, session_limit_prediction_baseline_data, session_limit_prediction_client_type, session_limit_prediction_details, session_limit_prediction_predict_request, session_limit_prediction_request, session_limit_prediction_result, session_limit_prediction_source, session_limit_prediction_tier, session_limit_prediction_tier_option, session_limit_prediction_unavailable_reason, session_list, session_list_entry, session_list_filter, session_load_deferred_repo_hooks_result, session_log_level, session_managed_permissions, session_managed_settings, session_mcp_apps_call_tool_result, session_mcp_oauth_cancel_login_request, session_mcp_oauth_cancel_login_result, session_mcp_oauth_prepare_login_request, session_mcp_oauth_prepare_login_result, session_metadata_snapshot, session_mode, session_model_list, session_model_list_request, session_model_price_category, session_open_options, session_open_options_additional_content_exclusion_policy, session_open_options_additional_content_exclusion_policy_rule, session_open_options_additional_content_exclusion_policy_rule_source, session_open_options_additional_content_exclusion_policy_scope, session_open_options_env_value_mode, session_open_options_reasoning_summary, session_open_params, session_open_result, session_plugins_disable_request, session_plugins_enable_request, session_plugins_install_request, session_plugins_marketplaces_refresh_request, session_plugins_reload_request, session_provider_get_endpoint_request, session_prune_result, sessions_bulk_delete_request, sessions_check_in_use_request, sessions_check_in_use_result, sessions_client_metadata_entry, sessions_close_request, sessions_close_result, sessions_delete_request, sessions_enrich_metadata_request, session_set_credentials_params, session_set_credentials_result, session_settings_built_in_tool_availability_snapshot, session_settings_evaluate_predicate_request, session_settings_evaluate_predicate_result, session_settings_job_snapshot, session_settings_model_snapshot, session_settings_online_evaluation_snapshot, session_settings_predicate_name, session_settings_repo_snapshot, session_settings_snapshot, session_settings_validation_snapshot, sessions_find_by_prefix_request, sessions_find_by_prefix_result, sessions_find_by_task_id_request, sessions_find_by_task_id_result, sessions_fork_request, sessions_fork_result, sessions_get_board_entry_count_request, sessions_get_board_entry_count_result, sessions_get_client_metadata_request, sessions_get_client_metadata_result, sessions_get_event_file_path_request, sessions_get_event_file_path_result, sessions_get_last_for_context_request, sessions_get_last_for_context_result, sessions_get_metadata_request, sessions_get_metadata_result, sessions_get_persisted_remote_steerable_request, sessions_get_persisted_remote_steerable_result, session_sizes, sessions_list_non_empty_session_ids_request, sessions_list_non_empty_session_ids_result, sessions_list_request, sessions_load_deferred_repo_hooks_request, sessions_open_attach, sessions_open_cloud, sessions_open_create, sessions_open_handoff, sessions_open_handoff_task_type, sessions_open_progress, sessions_open_progress_status, sessions_open_progress_step, sessions_open_remote, sessions_open_resume, sessions_open_resume_last, sessions_open_status, session_source, sessions_prune_old_request, sessions_read_persisted_events_request, sessions_release_lock_request, sessions_release_lock_result, sessions_reload_plugin_hooks_request, sessions_reload_plugin_hooks_result, sessions_save_request, sessions_save_result, sessions_set_additional_plugins_request, sessions_set_additional_plugins_result, sessions_set_remote_control_steering_request, sessions_start_remote_control_request, sessions_stop_remote_control_request, sessions_transfer_remote_control_request, session_telemetry_engagement, session_update_options_params, session_update_options_result, session_visibility_status, session_workflow_pause_at_checkpoint_result, session_working_directory_context, session_working_directory_context_host_type, settable_auth_info, settable_token_auth_info, shell_cancel_user_requested_request, shell_credentials, shell_exec_request, shell_exec_result, shell_execute_user_requested_request, shell_init_profile, shell_init_script, shell_init_script_shell, shell_kill_request, shell_kill_result, shell_kill_signal, shell_options, shutdown_request, skill, skill_apply_install_request, skill_apply_uninstall_request, skill_discovery_path, skill_discovery_path_list, skill_discovery_scope, skill_installation_failure_reason, skill_installation_file_review, skill_installation_location, skill_installation_management_outcome, skill_installation_management_result, skill_installation_operation_request, skill_installation_operation_status, skill_installation_outcome, skill_installation_ownership_state, skill_installation_result, skill_installation_review, skill_installation_scope, skill_installation_session_state, skill_installation_source, skill_installations_request, skill_installation_summary, skill_install_plan, skill_list, skill_plan_install_request, skill_plan_uninstall_request, skill_provider_descriptor, skill_provider_list_request, skill_provider_list_result, skill_provider_read_request, skill_provider_read_result, skills_config_set_disabled_skills_request, skills_config_set_skill_disabled_request, skills_disable_request, skills_discover_request, skills_enable_request, skill_set_enabled_request, skills_get_discovery_paths_request, skills_get_invoked_result, skills_invoked_skill, skills_load_diagnostics, skill_uninstall_plan, slash_command_add_timeline_entry_result, slash_command_agent_prompt_result, slash_command_completed_result, slash_command_info, slash_command_input, slash_command_input_choice, slash_command_input_completion, slash_command_invocation_result, slash_command_kind, slash_command_model_picker_dialog, slash_command_select_subcommand_option, slash_command_select_subcommand_result, slash_command_set_model_result, slash_command_set_plan_model_result, slash_command_show_dialog_result, slash_command_text_result, slash_command_timeline_entry, subagent_settings_entry, subagent_settings_entry_context_tier, system_message_block, task_agent_info, task_agent_progress, task_client_active_status, task_client_execution_mode, task_client_info, task_client_owner, task_client_owner_kind, task_client_owner_presence, task_client_progress, task_client_status, task_client_type, task_client_update, task_complete_data, task_completion_decision, task_execution_mode, task_info, task_kind, task_list, task_progress_line, tasks_cancel_request, tasks_cancel_result, tasks_get_current_promotable_result, tasks_get_progress_request, tasks_get_progress_result, task_shell_info, task_shell_info_attachment_mode, task_shell_progress, tasks_promote_current_to_background_result, tasks_promote_to_background_request, tasks_promote_to_background_result, tasks_refresh_result, tasks_register_request, tasks_register_result, tasks_remove_request, tasks_remove_result, tasks_send_message_request, tasks_send_message_result, tasks_start_agent_request, tasks_start_agent_result, task_status, tasks_update_request, tasks_update_result, tasks_wait_for_pending_result, telemetry_set_feature_overrides_request, token_auth_info, token_provider_auth_info, tool, tool_list, tool_result, tool_result_expanded, tool_result_new_message, tool_result_type, tools_execute_request, tools_get_builtin_descriptors_request, tools_get_builtin_descriptors_result, tools_get_current_metadata_result, tools_initialize_and_validate_result, tools_list_request, tools_set_request, tools_set_result, tools_shell_descriptor_config, tools_task_complete_event_data_request, tools_update_subagent_settings_result, ui_auto_mode_switch_response, ui_elicitation_array_any_of_field, ui_elicitation_array_any_of_field_items, ui_elicitation_array_any_of_field_items_any_of, ui_elicitation_array_enum_field, ui_elicitation_array_enum_field_items, ui_elicitation_field_value, ui_elicitation_request, ui_elicitation_response, ui_elicitation_response_action, ui_elicitation_response_content, ui_elicitation_result, ui_elicitation_schema, ui_elicitation_schema_property, ui_elicitation_schema_property_boolean, ui_elicitation_schema_property_number, ui_elicitation_schema_property_number_type, ui_elicitation_schema_property_string, ui_elicitation_schema_property_string_format, ui_elicitation_string_enum_field, ui_elicitation_string_one_of_field, ui_elicitation_string_one_of_field_one_of, ui_ephemeral_query_request, ui_ephemeral_query_result, ui_exit_plan_mode_action, ui_exit_plan_mode_response, ui_handle_pending_auto_mode_switch_request, ui_handle_pending_elicitation_request, ui_handle_pending_exit_plan_mode_request, ui_handle_pending_result, ui_handle_pending_sampling_request, ui_handle_pending_sampling_response, ui_handle_pending_session_limits_exhausted_request, ui_handle_pending_user_input_request, ui_register_direct_auto_mode_switch_handler_result, ui_session_limits_exhausted_response, ui_session_limits_exhausted_response_action, ui_unregister_direct_auto_mode_switch_handler_request, ui_unregister_direct_auto_mode_switch_handler_result, ui_user_input_response, unsupported_enqueue_command_result, update_subagent_settings_request, usage_get_metrics_result, usage_metrics_agent_metric, usage_metrics_code_changes, usage_metrics_model_metric, usage_metrics_model_metric_requests, usage_metrics_model_metric_token_detail, usage_metrics_model_metric_usage, usage_metrics_token_detail, user_auth_info, user_requested_shell_command_result, user_setting_metadata, user_settings_get_result, user_settings_set_request, user_settings_set_result, visibility_get_result, visibility_set_request, visibility_set_result, workflow_abort_request, workflow_ack_result, workflow_agent_options, workflow_agent_request, workflow_agent_result, workflow_agent_summary, workflow_cancel_request, workflow_current_phase, workflow_declared_limits, workflow_durable_operation, workflow_execute_request, workflow_execute_result, workflow_get_run_progress_request, workflow_get_run_request, workflow_journal_get_request, workflow_journal_get_result, workflow_journal_put_request, workflow_list_runs_request, workflow_list_runs_result, workflow_log_line, workflow_log_line_kind, workflow_log_request, workflow_pause_checkpoint_action, workflow_pause_checkpoint_request, workflow_pause_checkpoint_result, workflow_pause_info, workflow_pause_request, workflow_phase_observation, workflow_phase_status, workflow_progress_line, workflow_progress_page, workflow_resume_request, workflow_resume_result, workflow_run_consumed, workflow_run_detail, workflow_run_failure, workflow_run_failure_kind, workflow_run_limits, workflow_run_options, workflow_run_request, workflow_run_result, workflow_run_status, workflow_run_summary, workflow_run_terminal, workflow_tool_resume_request, workflow_tool_run_options, workflow_tool_run_request, workspace_diff_file_change, workspace_diff_file_change_type, workspace_diff_mode, workspace_diff_result, workspaces_add_summary_request, workspaces_add_summary_result, workspaces_autopilot_objective_exists_result, workspaces_checkpoints, workspaces_create_directory_request, workspaces_create_file_request, workspaces_delete_autopilot_objective_result, workspaces_diff_request, workspaces_ensure_request, workspaces_get_workspace_result, workspaces_list_checkpoints_result, workspaces_list_files_result, workspaces_read_autopilot_objective_result, workspaces_read_checkpoint_request, workspaces_read_checkpoint_result, workspaces_read_file_request, workspaces_read_file_result, workspaces_remove_path_request, workspaces_rename_path_request, workspaces_save_large_paste_request, workspaces_save_large_paste_result, workspaces_stat_file_request, workspaces_stat_file_result, workspaces_truncate_summaries_request, workspace_summary_host_type, workspaces_update_metadata_request, workspaces_workspace_details_host_type, workspaces_write_autopilot_objective_request, workspaces_write_autopilot_objective_result, connector_session_account_result, session_auth_info_result, session_context_attribution, session_context_info, subagent_settings, task_progress, workspace_summary) def to_dict(self) -> dict: result: dict = {} @@ -49644,6 +50737,10 @@ def to_dict(self) -> dict: result["CopilotUserResponseQuotaSnapshotsPremiumInteractions"] = to_class(CopilotUserResponseQuotaSnapshotsPremiumInteractions, self.copilot_user_response_quota_snapshots_premium_interactions) result["CurrentModel"] = to_class(CurrentModel, self.current_model) result["CurrentToolMetadata"] = to_class(CurrentToolMetadata, self.current_tool_metadata) + result["CustomizationReloadOutcome"] = to_class(CustomizationReloadOutcome, self.customization_reload_outcome) + result["CustomizationReloadStatus"] = to_enum(CustomizationReloadStatus, self.customization_reload_status) + result["CustomizationReloadSubsystem"] = to_enum(CustomizationReloadSubsystem, self.customization_reload_subsystem) + result["CustomizationsReloadResult"] = to_class(CustomizationsReloadResult, self.customizations_reload_result) result["DebugCollectLogsCollectedEntry"] = to_class(DebugCollectLogsCollectedEntry, self.debug_collect_logs_collected_entry) result["DebugCollectLogsDestination"] = to_class(DebugCollectLogsDestination, self.debug_collect_logs_destination) result["DebugCollectLogsEntry"] = to_class(DebugCollectLogsEntry, self.debug_collect_logs_entry) @@ -49676,11 +50773,10 @@ def to_dict(self) -> dict: result["DiscoveredHook"] = to_class(DiscoveredHook, self.discovered_hook) result["DiscoveredMcpServer"] = to_class(DiscoveredMCPServer, self.discovered_mcp_server) result["DiscoveredMcpServerType"] = to_enum(DiscoveredMCPServerType, self.discovered_mcp_server_type) + result["DiscoveredModel"] = to_class(DiscoveredModel, self.discovered_model) + result["DiscoveredModelList"] = to_class(DiscoveredModelList, self.discovered_model_list) result["EnqueueCommandParams"] = to_class(EnqueueCommandParams, self.enqueue_command_params) result["EnqueueCommandResult"] = (self.enqueue_command_result).to_dict() - result["EntraTokenAcquireRequest"] = to_class(EntraTokenAcquireRequest, self.entra_token_acquire_request) - result["EntraTokenAcquireResult"] = to_class(EntraTokenAcquireResult, self.entra_token_acquire_result) - result["EntraTokenInteraction"] = to_enum(EntraTokenInteraction, self.entra_token_interaction) result["EnvAuthInfo"] = to_class(EnvAuthInfo, self.env_auth_info) result["EnvironmentCapabilities"] = to_class(EnvironmentCapabilities, self.environment_capabilities) result["EnvironmentKind"] = to_enum(EnvironmentKind, self.environment_kind) @@ -50073,6 +51169,7 @@ def to_dict(self) -> dict: result["MetadataUpdateClientMetadataRequest"] = to_class(MetadataUpdateClientMetadataRequest, self.metadata_update_client_metadata_request) result["Model"] = to_class(Model, self.model) result["ModelApplyStartupOverlayRequest"] = to_class(ModelApplyStartupOverlayRequest, self.model_apply_startup_overlay_request) + result["ModelArtifactDetails"] = to_class(ModelArtifactDetails, self.model_artifact_details) result["ModelBilling"] = to_class(ModelBilling, self.model_billing) result["ModelBillingPromo"] = to_class(ModelBillingPromo, self.model_billing_promo) result["ModelBillingTokenPrices"] = to_class(ModelBillingTokenPrices, self.model_billing_token_prices) @@ -50094,9 +51191,31 @@ def to_dict(self) -> dict: result["ModelPickerSettingsContext"] = to_class(ModelPickerSettingsContext, self.model_picker_settings_context) result["ModelPolicy"] = to_class(ModelPolicy, self.model_policy) result["ModelPolicyState"] = to_enum(ModelPolicyState, self.model_policy_state) + result["ModelProviderAdapterCatalog"] = to_class(ModelProviderAdapterCatalog, self.model_provider_adapter_catalog) + result["ModelProviderAdapterDescriptor"] = to_class(ModelProviderAdapterDescriptor, self.model_provider_adapter_descriptor) + result["ModelProviderAdapterOperationDescriptor"] = to_class(ModelProviderAdapterOperationDescriptor, self.model_provider_adapter_operation_descriptor) + result["ModelProviderAttribution"] = to_class(ModelProviderAttribution, self.model_provider_attribution) + result["ModelProviderAutomaticDiscoveryMode"] = to_enum(ModelProviderAutomaticDiscoveryMode, self.model_provider_automatic_discovery_mode) + result["ModelProviderAutomaticDiscoveryPolicy"] = to_class(ModelProviderAutomaticDiscoveryPolicy, self.model_provider_automatic_discovery_policy) + result["ModelProviderConfigurationDisposition"] = to_enum(ModelProviderConfigurationDisposition, self.model_provider_configuration_disposition) + result["ModelProviderConfigurationPlan"] = to_class(ModelProviderConfigurationPlan, self.model_provider_configuration_plan) result["ModelProviderDescriptor"] = to_class(ModelProviderDescriptor, self.model_provider_descriptor) + result["ModelProviderDiscoverRequest"] = to_class(ModelProviderDiscoverRequest, self.model_provider_discover_request) + result["ModelProviderDiscoverResult"] = to_class(ModelProviderDiscoverResult, self.model_provider_discover_result) + result["ModelProviderDiscoveryNetworkScope"] = to_enum(ModelProviderDiscoveryNetworkScope, self.model_provider_discovery_network_scope) + result["ModelProviderGetStatusRequest"] = to_class(ModelProviderGetStatusRequest, self.model_provider_get_status_request) + result["ModelProviderInstance"] = to_class(ModelProviderInstance, self.model_provider_instance) + result["ModelProviderInstanceReference"] = to_class(ModelProviderInstanceReference, self.model_provider_instance_reference) result["ModelProviderKind"] = to_enum(ModelProviderKind, self.model_provider_kind) + result["ModelProviderModelsListRequest"] = to_class(ModelProviderModelsListRequest, self.model_provider_models_list_request) + result["ModelProviderOperationOutcome"] = to_class(ModelProviderOperationOutcome, self.model_provider_operation_outcome) + result["ModelProviderOperationOutcomeCode"] = to_enum(ModelProviderOperationOutcomeCode, self.model_provider_operation_outcome_code) + result["ModelProviderPrepareConfigurationRequest"] = to_class(ModelProviderPrepareConfigurationRequest, self.model_provider_prepare_configuration_request) + result["ModelProviderProvenance"] = to_class(ModelProviderProvenance, self.model_provider_provenance) + result["ModelProviderProvenanceSource"] = to_enum(ModelProviderProvenanceSource, self.model_provider_provenance_source) result["ModelProviderRef"] = to_class(ModelProviderRef, self.model_provider_ref) + result["ModelProviderStatus"] = to_class(ModelProviderStatus, self.model_provider_status) + result["ModelProviderWarning"] = to_class(ModelProviderWarning, self.model_provider_warning) result["ModelSetAllowedModelsRequest"] = to_class(ModelSetAllowedModelsRequest, self.model_set_allowed_models_request) result["ModelSetAllowedModelsResult"] = to_class(ModelSetAllowedModelsResult, self.model_set_allowed_models_result) result["ModelSetReasoningEffortRequest"] = to_class(ModelSetReasoningEffortRequest, self.model_set_reasoning_effort_request) @@ -50397,6 +51516,10 @@ def to_dict(self) -> dict: result["SandboxHostCapabilityName"] = from_str(self.sandbox_host_capability_name) result["SandboxHostSupport"] = to_class(SandboxHostSupport, self.sandbox_host_support) result["SandboxMaskedEnvVar"] = to_class(SandboxMaskedEnvVar, self.sandbox_masked_env_var) + result["SandboxProxyCaCreateResult"] = to_class(SandboxProxyCACreateResult, self.sandbox_proxy_ca_create_result) + result["SandboxProxyCaRequest"] = to_class(SandboxProxyCARequest, self.sandbox_proxy_ca_request) + result["SandboxProxyCaState"] = to_enum(SandboxProxyCAState, self.sandbox_proxy_ca_state) + result["SandboxProxyCaStatus"] = to_class(SandboxProxyCAStatus, self.sandbox_proxy_ca_status) result["SandboxSessionChange"] = to_enum(SandboxSessionChange, self.sandbox_session_change) result["ScheduleAddAtRequest"] = to_class(ScheduleAddAtRequest, self.schedule_add_at_request) result["ScheduleAddCronRequest"] = to_class(ScheduleAddCronRequest, self.schedule_add_cron_request) @@ -50450,6 +51573,8 @@ def to_dict(self) -> dict: result["SessionFsReaddirWithTypesEntryType"] = to_enum(DebugCollectLogsEntryKind, self.session_fs_readdir_with_types_entry_type) result["SessionFsReaddirWithTypesRequest"] = to_class(SessionFSReaddirWithTypesRequest, self.session_fs_readdir_with_types_request) result["SessionFsReaddirWithTypesResult"] = to_class(SessionFSReaddirWithTypesResult, self.session_fs_readdir_with_types_result) + result["SessionFsReadFileBytesRequest"] = to_class(SessionFSReadFileBytesRequest, self.session_fs_read_file_bytes_request) + result["SessionFsReadFileBytesResult"] = to_class(SessionFSReadFileBytesResult, self.session_fs_read_file_bytes_result) result["SessionFsReadFileRequest"] = to_class(SessionFSReadFileRequest, self.session_fs_read_file_request) result["SessionFsReadFileResult"] = to_class(SessionFSReadFileResult, self.session_fs_read_file_result) result["SessionFsRenameRequest"] = to_class(SessionFSRenameRequest, self.session_fs_rename_request) @@ -50470,6 +51595,7 @@ def to_dict(self) -> dict: result["SessionFsSqliteTransactionStatement"] = to_class(SessionFSSqliteTransactionStatement, self.session_fs_sqlite_transaction_statement) result["SessionFsStatRequest"] = to_class(SessionFSStatRequest, self.session_fs_stat_request) result["SessionFsStatResult"] = to_class(SessionFSStatResult, self.session_fs_stat_result) + result["SessionFsWriteFileBytesRequest"] = to_class(SessionFSWriteFileBytesRequest, self.session_fs_write_file_bytes_request) result["SessionFsWriteFileRequest"] = to_class(SessionFSWriteFileRequest, self.session_fs_write_file_request) result["SessionGitHubAuthGetAllAuthAvailableResult"] = from_list(lambda x: to_class(SessionAuthStatus, x), self.session_git_hub_auth_get_all_auth_available_result) result["SessionGitHubAuthLogoutResult"] = from_bool(self.session_git_hub_auth_logout_result) @@ -51470,10 +52596,41 @@ async def get_built_in_catalog(self, *, timeout: float | None = None) -> BuiltIn return BuiltInModelCatalog.from_dict(await self._client.request("models.getBuiltInCatalog", {}, **_timeout_kwargs(timeout))) +# Experimental: this API group is experimental and may change or be removed. +class ServerSandboxProxyCaApi: + def __init__(self, client: "JsonRpcClient"): + self._client = client + + async def get_status(self, params: SandboxProxyCARequest, *, timeout: float | None = None) -> SandboxProxyCAStatus: + "Reports whether the persistent certificate authority of the sandbox credential proxy exists, whether OS trust includes it, and whether it must be rotated. Changes nothing.\n\nArgs:\n params: Identifies the credential hosts that the persistent certificate authority of the sandbox credential proxy must cover. The runtime always adds the hosts from the saved user settings.\n\nReturns:\n Status of the persistent certificate authority of the sandbox credential proxy." + params_dict = {k: v for k, v in params.to_dict().items() if v is not None} + return SandboxProxyCAStatus.from_dict(await self._client.request("sandbox.proxyCa.getStatus", params_dict, **_timeout_kwargs(timeout))) + + async def create(self, params: SandboxProxyCARequest, *, timeout: float | None = None) -> SandboxProxyCACreateResult: + "Creates the persistent certificate authority of the sandbox credential proxy if none is stored, without changing OS trust, and returns the path of its public certificate. Keeps an existing certificate authority, even one that must be rotated. Fails where OS trust is unsupported. Trust it with sandbox.proxyCa.trust: the CLI trusts only the hosts in the saved user settings, so it refuses a certificate authority that also covers hosts from sandboxConfig.\n\nArgs:\n params: Identifies the credential hosts that the persistent certificate authority of the sandbox credential proxy must cover. The runtime always adds the hosts from the saved user settings.\n\nReturns:\n Result of creating the persistent certificate authority of the sandbox credential proxy." + params_dict = {k: v for k, v in params.to_dict().items() if v is not None} + return SandboxProxyCACreateResult.from_dict(await self._client.request("sandbox.proxyCa.create", params_dict, **_timeout_kwargs(timeout))) + + async def rotate(self, params: SandboxProxyCARequest, *, timeout: float | None = None) -> SandboxProxyCAStatus: + "Replaces the persistent certificate authority of the sandbox credential proxy with a new one for the current credential hosts. If OS trust included the old one, removes it and trusts the new one, which can show an OS authentication prompt. Running sandboxed tools keep the old certificate authority until they restart.\n\nArgs:\n params: Identifies the credential hosts that the persistent certificate authority of the sandbox credential proxy must cover. The runtime always adds the hosts from the saved user settings.\n\nReturns:\n Status of the persistent certificate authority of the sandbox credential proxy." + params_dict = {k: v for k, v in params.to_dict().items() if v is not None} + return SandboxProxyCAStatus.from_dict(await self._client.request("sandbox.proxyCa.rotate", params_dict, **_timeout_kwargs(timeout))) + + async def trust(self, params: SandboxProxyCARequest, *, timeout: float | None = None) -> SandboxProxyCAStatus: + "Adds the persistent certificate authority of the sandbox credential proxy to OS trust, so sandboxed clients that read only OS trust accept the proxy. Call create first. Refuses a certificate authority that is not constrained to the current credential hosts. Can show an OS authentication prompt.\n\nArgs:\n params: Identifies the credential hosts that the persistent certificate authority of the sandbox credential proxy must cover. The runtime always adds the hosts from the saved user settings.\n\nReturns:\n Status of the persistent certificate authority of the sandbox credential proxy." + params_dict = {k: v for k, v in params.to_dict().items() if v is not None} + return SandboxProxyCAStatus.from_dict(await self._client.request("sandbox.proxyCa.trust", params_dict, **_timeout_kwargs(timeout))) + + async def remove(self, *, timeout: float | None = None) -> SandboxProxyCAStatus: + "Removes the persistent certificate authority of the sandbox credential proxy from OS trust. Keeps the stored certificate authority. Can show an OS authentication prompt. Sandboxed clients that read only OS trust then reject the proxy; clients that read the per-process certificate bundle continue to work.\n\nReturns:\n Status of the persistent certificate authority of the sandbox credential proxy." + return SandboxProxyCAStatus.from_dict(await self._client.request("sandbox.proxyCa.remove", {}, **_timeout_kwargs(timeout))) + + # Experimental: this API group is experimental and may change or be removed. class ServerSandboxApi: def __init__(self, client: "JsonRpcClient"): self._client = client + self.proxy_ca = ServerSandboxProxyCaApi(client) async def get_host_support(self, *, timeout: float | None = None) -> SandboxHostSupport: "Reports whether the host running this runtime can run the command sandbox, without starting a session or spawning a sandboxed command.\n\nReturns:\n Whether the host running this runtime can run the command sandbox. The runtime checks `supported` once per process. A capability answer can change while the process runs, for example after the user installs a missing package." @@ -52227,24 +53384,12 @@ async def _configure_session_extensions(self, params: _ConfigureSessionExtension await self._client.request("sessions.configureSessionExtensions", params_dict, **_timeout_kwargs(timeout)) -# Experimental: this API group is experimental and may change or be removed. -class _InternalServerAccountsApi: - def __init__(self, client: "JsonRpcClient"): - self._client = client - - async def _acquire_entra_token(self, params: EntraTokenAcquireRequest, *, timeout: float | None = None) -> EntraTokenAcquireResult: - "Acquire a Microsoft Entra access token through the runtime's OneAuth broker. Account-scoped because it uses the same native broker as the account stack: a trusted host application mints a scoped Entra token for its own use, most notably to authenticate to a remote MCP server whose authorization server is Entra ID (in place of the generic browser-OAuth flow).\n\nArgs:\n params: OneAuth token request supplied by a trusted host application.\n\nReturns:\n Result of a OneAuth token acquisition.\n\n:meta private:\n\nInternal SDK API; not part of the public surface." - params_dict = {k: v for k, v in params.to_dict().items() if v is not None} - return EntraTokenAcquireResult.from_dict(await self._client.request("accounts.acquireEntraToken", params_dict, **_timeout_kwargs(timeout))) - - class _InternalServerRpc: """Internal SDK server-scoped RPC methods. Not part of the public API.""" def __init__(self, client: "JsonRpcClient"): self._client = client self.host = _InternalServerHostApi(client) self.sessions = _InternalServerSessionsApi(client) - self.accounts = _InternalServerAccountsApi(client) async def _connect(self, params: _ConnectRequest, *, timeout: float | None = None) -> _ConnectResult: "Performs the SDK server connection handshake and validates the optional connection token. Marked internal because this is JSON-RPC transport plumbing invoked automatically by an SDK client's own `connect()` wrapper, not a user-facing method. Stays internal as long as the SDK client owns the handshake; would only become public if the SDK ever exposed the raw schema surface to consumers without a connection wrapper.\n\nArgs:\n params: Connection-level opt-ins for the `server.connect` handshake. Transport authentication is consumed by the native protocol boundary before dispatch.\n\nReturns:\n Handshake result reporting the server's protocol version and package version on success.\n\n.. warning:: This API is experimental and may change or be removed in future versions.\n\n:meta private:\n\nInternal SDK API; not part of the public surface." @@ -52252,6 +53397,49 @@ async def _connect(self, params: _ConnectRequest, *, timeout: float | None = Non return _ConnectResult.from_dict(await self._client.request("connect", params_dict, **_timeout_kwargs(timeout))) +# Experimental: this API group is experimental and may change or be removed. +class ProvidersModelsApi: + def __init__(self, client: "JsonRpcClient", session_id: str): + self._client = client + self._session_id = session_id + + async def list(self, params: ModelProviderModelsListRequest, *, timeout: float | None = None) -> DiscoveredModelList: + "Lists models installed or otherwise available from a discovered model-provider instance.\n\nArgs:\n params: Provider model inventory request parameters.\n\nReturns:\n Models offered for agent conversations by one provider instance. Adapters exclude known-incompatible models, but retain candidates with unknown capabilities. Listing does not guarantee compatibility." + params_dict: dict[str, Any] = {k: v for k, v in params.to_dict().items() if v is not None} + params_dict["sessionId"] = self._session_id + return DiscoveredModelList.from_dict(await self._client.request("session.providers.models.list", params_dict, **_timeout_kwargs(timeout))) + + async def prepare_configuration(self, params: ModelProviderPrepareConfigurationRequest, *, timeout: float | None = None) -> ModelProviderConfigurationPlan: + "Translates a discovered model into the provider and model configuration needed to use it, and reports whether each is already registered in this session. Prepares only: it registers nothing, writes nothing, and performs no provider requests.\n\nArgs:\n params: A discovered instance and one of its models to translate into provider configuration. Pass back the instance and model as returned by `session.providers.discover` and `session.providers.models.list`.\n\nReturns:\n Provider configuration prepared from a discovered model. Preparing a plan changes nothing: it neither registers the model with the session nor writes durable configuration. To apply it, pass `provider` and `model` to `session.provider.add`, omitting whichever the dispositions report as already configured." + params_dict: dict[str, Any] = {k: v for k, v in params.to_dict().items() if v is not None} + params_dict["sessionId"] = self._session_id + return ModelProviderConfigurationPlan.from_dict(await self._client.request("session.providers.models.prepareConfiguration", params_dict, **_timeout_kwargs(timeout))) + + +# Experimental: this API group is experimental and may change or be removed. +class ProvidersApi: + def __init__(self, client: "JsonRpcClient", session_id: str): + self._client = client + self._session_id = session_id + self.models = ProvidersModelsApi(client, session_id) + + async def get_catalog(self, *, timeout: float | None = None) -> ModelProviderAdapterCatalog: + "Returns adapter definitions and supported operations in this session's effective provider catalog, without running discovery. Does not list provider instances or select inference models.\n\nReturns:\n Normalized model-provider adapter definitions available to the session, not discovered instances." + return ModelProviderAdapterCatalog.from_dict(await self._client.request("session.providers.getCatalog", {"sessionId": self._session_id}, **_timeout_kwargs(timeout))) + + async def discover(self, params: ModelProviderDiscoverRequest, *, timeout: float | None = None) -> ModelProviderDiscoverResult: + "Discovers reachable instances using an adapter from this session's effective provider catalog and provider-specific discovery input.\n\nArgs:\n params: Provider discovery parameters.\n\nReturns:\n Provider instances found by a discovery operation." + params_dict: dict[str, Any] = {k: v for k, v in params.to_dict().items() if v is not None} + params_dict["sessionId"] = self._session_id + return ModelProviderDiscoverResult.from_dict(await self._client.request("session.providers.discover", params_dict, **_timeout_kwargs(timeout))) + + async def get_status(self, params: ModelProviderGetStatusRequest, *, timeout: float | None = None) -> ModelProviderStatus: + "Gets current health and version information for a discovered model-provider instance.\n\nArgs:\n params: Provider status request parameters.\n\nReturns:\n Current health information for a provider instance." + params_dict: dict[str, Any] = {k: v for k, v in params.to_dict().items() if v is not None} + params_dict["sessionId"] = self._session_id + return ModelProviderStatus.from_dict(await self._client.request("session.providers.getStatus", params_dict, **_timeout_kwargs(timeout))) + + # Experimental: this API group is experimental and may change or be removed. class SandboxApi: def __init__(self, client: "JsonRpcClient", session_id: str): @@ -52747,9 +53935,9 @@ async def get_sources(self, *, timeout: float | None = None) -> InstructionsGetS "Gets instruction sources loaded for the session.\n\nReturns:\n Instruction sources loaded for the session, in merge order." return InstructionsGetSourcesResult.from_dict(await self._client.request("session.instructions.getSources", {"sessionId": self._session_id}, **_timeout_kwargs(timeout))) - async def reload(self, *, timeout: float | None = None) -> None: - "Invalidates cached custom-instruction discovery so subsequent turns and source reads observe instruction files currently on disk." - await self._client.request("session.instructions.reload", {"sessionId": self._session_id}, **_timeout_kwargs(timeout)) + async def reload(self, *, timeout: float | None = None) -> InstructionsGetSourcesResult: + "For local sessions, invalidates instruction discovery and the model-facing prompt, then returns freshly discovered sources. The updated prompt takes effect on the next turn. Remote sessions must reload on their agent host instead.\n\nReturns:\n Instruction sources loaded for the session, in merge order." + return InstructionsGetSourcesResult.from_dict(await self._client.request("session.instructions.reload", {"sessionId": self._session_id}, **_timeout_kwargs(timeout))) # Experimental: this API group is experimental and may change or be removed. @@ -52758,9 +53946,9 @@ def __init__(self, client: "JsonRpcClient", session_id: str): self._client = client self._session_id = session_id - async def reload(self, *, timeout: float | None = None) -> SkillsLoadDiagnostics: - "Reloads all repository and user customizations for the active session: instructions, plugins and their MCP servers and hooks, custom agents, extensions, and skills. Returns diagnostics from the final skill reload.\n\nReturns:\n Diagnostics from reloading skill definitions, with warnings and errors as separate lists." - return SkillsLoadDiagnostics.from_dict(await self._client.request("session.customizations.reload", {"sessionId": self._session_id}, **_timeout_kwargs(timeout))) + async def reload(self, *, timeout: float | None = None) -> CustomizationsReloadResult: + "For local sessions, reconciles repository context and discovered instructions, plugins, skills, agents, hooks, MCP servers, and extensions after files appear or change under the working directory. Independent component failures are returned in outcomes and errors; a rejected call can have partially applied earlier steps. Remote sessions must reload on their agent host instead. The model-facing context is rebuilt on the next turn.\n\nReturns:\n Results of reloading discovered session customizations. Inspect outcomes for reloaded, skipped, or failed subsystems; a rejection may follow partial mutation. Changes to the model-facing prompt and tools apply on the next turn." + return CustomizationsReloadResult.from_dict(await self._client.request("session.customizations.reload", {"sessionId": self._session_id}, **_timeout_kwargs(timeout))) # Experimental: this API group is experimental and may change or be removed. @@ -54112,6 +55300,7 @@ class SessionRpc: def __init__(self, client: "JsonRpcClient", session_id: str): self._client = client self._session_id = session_id + self.providers = ProvidersApi(client, session_id) self.sandbox = SandboxApi(client, session_id) self.git_hub_auth = GitHubAuthApi(client, session_id) self.accounts = AccountsApi(client, session_id) @@ -54375,6 +55564,31 @@ async def _finalize_invocation_effect(self, params: CommandsFinalizeInvocationEf return CommandsFinalizeInvocationEffectResult.from_dict(await self._client.request("session.commands.finalizeInvocationEffect", params_dict, **_timeout_kwargs(timeout))) +# Experimental: this API group is experimental and may change or be removed. +class _InternalUiApi: + def __init__(self, client: "JsonRpcClient", session_id: str): + self._client = client + self._session_id = session_id + + async def _handle_human_ask_user(self, params: UIHandlePendingElicitationRequest, *, timeout: float | None = None) -> UIElicitationResult: + "Resolves a pending elicitation request after direct interaction in the trusted in-process client. Only an accepted response to the built-in ask_user tool can become trusted human evidence.\n\nArgs:\n params: Pending elicitation request ID and the user's response (accept/decline/cancel + form values).\n\nReturns:\n Indicates whether the elicitation response was accepted; false if it was already resolved by another client.\n\n:meta private:\n\nInternal SDK API; not part of the public surface." + params_dict: dict[str, Any] = {k: v for k, v in params.to_dict().items() if v is not None} + params_dict["sessionId"] = self._session_id + return UIElicitationResult.from_dict(await self._client.request("session.ui.handleHumanAskUser", params_dict, **_timeout_kwargs(timeout))) + + async def _handle_human_user_input(self, params: UIHandlePendingUserInputRequest, *, timeout: float | None = None) -> UIHandlePendingResult: + "Resolves a pending `user_input.requested` event after direct interaction in the trusted in-process client.\n\nArgs:\n params: Request ID of a pending `user_input.requested` event and the user's response.\n\nReturns:\n Indicates whether the pending UI request was resolved by this call.\n\n:meta private:\n\nInternal SDK API; not part of the public surface." + params_dict: dict[str, Any] = {k: v for k, v in params.to_dict().items() if v is not None} + params_dict["sessionId"] = self._session_id + return UIHandlePendingResult.from_dict(await self._client.request("session.ui.handleHumanUserInput", params_dict, **_timeout_kwargs(timeout))) + + async def _handle_human_exit_plan_mode(self, params: UIHandlePendingExitPlanModeRequest, *, timeout: float | None = None) -> UIHandlePendingResult: + "Resolves a pending `exit_plan_mode.requested` event after direct interaction in the trusted in-process client.\n\nArgs:\n params: Request ID of a pending `exit_plan_mode.requested` event and the user's response.\n\nReturns:\n Indicates whether the pending UI request was resolved by this call.\n\n:meta private:\n\nInternal SDK API; not part of the public surface." + params_dict: dict[str, Any] = {k: v for k, v in params.to_dict().items() if v is not None} + params_dict["sessionId"] = self._session_id + return UIHandlePendingResult.from_dict(await self._client.request("session.ui.handleHumanExitPlanMode", params_dict, **_timeout_kwargs(timeout))) + + # Experimental: this API group is experimental and may change or be removed. class _InternalSettingsApi: def __init__(self, client: "JsonRpcClient", session_id: str): @@ -54496,6 +55710,7 @@ def __init__(self, client: "JsonRpcClient", session_id: str): self.mcp = _InternalMcpApi(client, session_id) self.connectors = _InternalConnectorsApi(client, session_id) self.commands = _InternalCommandsApi(client, session_id) + self.ui = _InternalUiApi(client, session_id) self.settings = _InternalSettingsApi(client, session_id) self.queue = _InternalQueueApi(client, session_id) self.schedule = _InternalScheduleApi(client, session_id) @@ -54533,9 +55748,15 @@ class SessionFsHandler(Protocol): async def read_file(self, params: SessionFSReadFileRequest) -> SessionFSReadFileResult: "Reads a file from the client-provided session filesystem.\n\nArgs:\n params: Path of the file to read from the client-provided session filesystem.\n\nReturns:\n File content as a UTF-8 string, or a filesystem error if the read failed." pass + async def read_file_bytes(self, params: SessionFSReadFileBytesRequest) -> SessionFSReadFileBytesResult: + "Reads binary file content from the client-provided session filesystem.\n\nArgs:\n params: Path of the binary file to read from the client-provided session filesystem.\n\nReturns:\n File bytes as standard base64, or a filesystem error if the read failed." + pass async def write_file(self, params: SessionFSWriteFileRequest) -> SessionFSError | None: "Writes a file in the client-provided session filesystem.\n\nArgs:\n params: File path, content to write, and optional mode for the client-provided session filesystem.\n\nReturns:\n Describes a filesystem error." pass + async def write_file_bytes(self, params: SessionFSWriteFileBytesRequest) -> SessionFSError | None: + "Writes binary file content to the client-provided session filesystem.\n\nArgs:\n params: File path, standard-base64-encoded bytes to write, and optional mode for the client-provided session filesystem.\n\nReturns:\n Describes a filesystem error." + pass async def append_file(self, params: SessionFSAppendFileRequest) -> SessionFSError | None: "Appends content to a file in the client-provided session filesystem, creating parent directories as needed.\n\nArgs:\n params: File path, content to append, and optional mode for the client-provided session filesystem. Implementations create parent directories as needed.\n\nReturns:\n Describes a filesystem error." pass @@ -54630,6 +55851,13 @@ async def handle_session_fs_read_file(params: dict) -> dict | None: result = await handler.read_file(request) return result.to_dict() client.set_request_handler("sessionFs.readFile", handle_session_fs_read_file) + async def handle_session_fs_read_file_bytes(params: dict) -> dict | None: + request = SessionFSReadFileBytesRequest.from_dict(params) + handler = get_handlers(request.session_id).session_fs + if handler is None: raise RuntimeError(f"No session_fs handler registered for session: {request.session_id}") + result = await handler.read_file_bytes(request) + return result.to_dict() + client.set_request_handler("sessionFs.readFileBytes", handle_session_fs_read_file_bytes) async def handle_session_fs_write_file(params: dict) -> dict | None: request = SessionFSWriteFileRequest.from_dict(params) handler = get_handlers(request.session_id).session_fs @@ -54637,6 +55865,13 @@ async def handle_session_fs_write_file(params: dict) -> dict | None: result = await handler.write_file(request) return result.to_dict() if result is not None else None client.set_request_handler("sessionFs.writeFile", handle_session_fs_write_file) + async def handle_session_fs_write_file_bytes(params: dict) -> dict | None: + request = SessionFSWriteFileBytesRequest.from_dict(params) + handler = get_handlers(request.session_id).session_fs + if handler is None: raise RuntimeError(f"No session_fs handler registered for session: {request.session_id}") + result = await handler.write_file_bytes(request) + return result.to_dict() if result is not None else None + client.set_request_handler("sessionFs.writeFileBytes", handle_session_fs_write_file_bytes) async def handle_session_fs_append_file(params: dict) -> dict | None: request = SessionFSAppendFileRequest.from_dict(params) handler = get_handlers(request.session_id).session_fs @@ -55217,7 +56452,11 @@ async def handle_installations_confirm(params: dict) -> dict | None: "CurrentModel", "CurrentToolMetadata", "CursorStatus", + "CustomizationReloadOutcome", + "CustomizationReloadStatus", + "CustomizationReloadSubsystem", "CustomizationsApi", + "CustomizationsReloadResult", "DebugApi", "DebugCollectLogsCollectedEntry", "DebugCollectLogsDestination", @@ -55252,13 +56491,11 @@ async def handle_installations_confirm(params: dict) -> dict | None: "DiscoveredHook", "DiscoveredMCPServer", "DiscoveredMCPServerType", + "DiscoveredModel", + "DiscoveredModelList", "EnqueueCommandParams", "EnqueueCommandResult", "EnterpriseList", - "EntraTokenAcquireRequest", - "EntraTokenAcquireResult", - "EntraTokenAcquireResultStatus", - "EntraTokenInteraction", "Entry", "EnvAuthInfo", "EnvAuthInfoType", @@ -55722,6 +56959,7 @@ async def handle_installations_confirm(params: dict) -> dict | None: "Model", "ModelApi", "ModelApplyStartupOverlayRequest", + "ModelArtifactDetails", "ModelBilling", "ModelBillingPromo", "ModelBillingTokenPrices", @@ -55743,9 +56981,31 @@ async def handle_installations_confirm(params: dict) -> dict | None: "ModelPickerSettingsContext", "ModelPolicy", "ModelPolicyState", + "ModelProviderAdapterCatalog", + "ModelProviderAdapterDescriptor", + "ModelProviderAdapterOperationDescriptor", + "ModelProviderAttribution", + "ModelProviderAutomaticDiscoveryMode", + "ModelProviderAutomaticDiscoveryPolicy", + "ModelProviderConfigurationDisposition", + "ModelProviderConfigurationPlan", "ModelProviderDescriptor", + "ModelProviderDiscoverRequest", + "ModelProviderDiscoverResult", + "ModelProviderDiscoveryNetworkScope", + "ModelProviderGetStatusRequest", + "ModelProviderInstance", + "ModelProviderInstanceReference", "ModelProviderKind", + "ModelProviderModelsListRequest", + "ModelProviderOperationOutcome", + "ModelProviderOperationOutcomeCode", + "ModelProviderPrepareConfigurationRequest", + "ModelProviderProvenance", + "ModelProviderProvenanceSource", "ModelProviderRef", + "ModelProviderStatus", + "ModelProviderWarning", "ModelSetAllowedModelsRequest", "ModelSetAllowedModelsResult", "ModelSetReasoningEffortRequest", @@ -55991,6 +57251,8 @@ async def handle_installations_confirm(params: dict) -> dict | None: "ProviderWireAPI", "ProviderWithdrawRequest", "ProviderWithdrawResult", + "ProvidersApi", + "ProvidersModelsApi", "PurpleSource", "PushAttachment", "PushAttachmentBlob", @@ -56119,6 +57381,10 @@ async def handle_installations_confirm(params: dict) -> dict | None: "SandboxHostCapabilityName", "SandboxHostSupport", "SandboxMaskedEnvVar", + "SandboxProxyCACreateResult", + "SandboxProxyCARequest", + "SandboxProxyCAState", + "SandboxProxyCAStatus", "SandboxSessionChange", "Saved", "ScheduleAddAtRequest", @@ -56168,6 +57434,7 @@ async def handle_installations_confirm(params: dict) -> dict | None: "ServerRpc", "ServerRuntimeApi", "ServerSandboxApi", + "ServerSandboxProxyCaApi", "ServerSecretsApi", "ServerSessionFsApi", "ServerSessionsApi", @@ -56202,6 +57469,8 @@ async def handle_installations_confirm(params: dict) -> dict | None: "SessionFSExistsRequest", "SessionFSExistsResult", "SessionFSMkdirRequest", + "SessionFSReadFileBytesRequest", + "SessionFSReadFileBytesResult", "SessionFSReadFileRequest", "SessionFSReadFileResult", "SessionFSReaddirRequest", @@ -56227,6 +57496,7 @@ async def handle_installations_confirm(params: dict) -> dict | None: "SessionFSSqliteTransactionStatement", "SessionFSStatRequest", "SessionFSStatResult", + "SessionFSWriteFileBytesRequest", "SessionFSWriteFileRequest", "SessionFsHandler", "SessionFsReaddirWithTypesEntryType", diff --git a/python/copilot/generated/session_events.py b/python/copilot/generated/session_events.py index 49278a75de..bec6e669ef 100644 --- a/python/copilot/generated/session_events.py +++ b/python/copilot/generated/session_events.py @@ -205,6 +205,7 @@ class SessionEventType(Enum): TOOL_USER_REQUESTED = "tool.user_requested" TOOL_EXECUTION_START = "tool.execution_start" TOOL_EXECUTION_PARTIAL_RESULT = "tool.execution_partial_result" + TOOL_SHELL_OUTPUT = "tool.shell_output" TOOL_EXECUTION_PROGRESS = "tool.execution_progress" TOOL_EXECUTION_COMPLETE = "tool.execution_complete" TOOL_SEARCH_ACTIVATED = "tool_search.activated" @@ -275,6 +276,7 @@ class SessionEventType(Enum): CAPABILITIES_CHANGED = "capabilities.changed" EXIT_PLAN_MODE_REQUESTED = "exit_plan_mode.requested" EXIT_PLAN_MODE_COMPLETED = "exit_plan_mode.completed" + HUMAN_RESPONSE_RECORDED = "human_response.recorded" SESSION_TOOLS_UPDATED = "session.tools_updated" SESSION_BACKGROUND_TASKS_CHANGED = "session.background_tasks_changed" # Experimental: this event is part of an experimental API and may change or be removed. @@ -429,6 +431,30 @@ def to_dict(self) -> dict: return result +# Deprecated: this type is deprecated and will be removed in a future version. +@dataclass +class ToolExecutionPartialResultData: + "Deprecated merged replacement snapshot of shell output. Use tool.shell_output for append-only, stream-tagged output instead." + partial_output: str + tool_call_id: str + + @staticmethod + def from_dict(obj: Any) -> "ToolExecutionPartialResultData": + assert isinstance(obj, dict) + partial_output = from_str(obj.get("partialOutput")) + tool_call_id = from_str(obj.get("toolCallId")) + return ToolExecutionPartialResultData( + partial_output=partial_output, + tool_call_id=tool_call_id, + ) + + def to_dict(self) -> dict: + result: dict = {} + result["partialOutput"] = from_str(self.partial_output) + result["toolCallId"] = from_str(self.tool_call_id) + return result + + # Experimental: this type is part of an experimental API and may change or be removed. @dataclass class AssistantFusionPhaseActivityData: @@ -695,15 +721,18 @@ class AssistantMessageReasoningBlocks: "Neutral provider-tagged reasoning content blocks preserved verbatim for round-tripping" provider: str blocks: list[Any] | None = None + ordered_blocks: list[Any] | None = None @staticmethod def from_dict(obj: Any) -> "AssistantMessageReasoningBlocks": assert isinstance(obj, dict) provider = from_str(obj.get("provider")) blocks = from_union([from_none, lambda x: from_list(lambda x: x, x)], obj.get("blocks")) + ordered_blocks = from_union([from_none, lambda x: from_list(lambda x: x, x)], obj.get("orderedBlocks")) return AssistantMessageReasoningBlocks( provider=provider, blocks=blocks, + ordered_blocks=ordered_blocks, ) def to_dict(self) -> dict: @@ -711,6 +740,8 @@ def to_dict(self) -> dict: result["provider"] = from_str(self.provider) if self.blocks is not None: result["blocks"] = from_union([from_none, lambda x: from_list(lambda x: x, x)], self.blocks) + if self.ordered_blocks is not None: + result["orderedBlocks"] = from_union([from_none, lambda x: from_list(lambda x: x, x)], self.ordered_blocks) return result @@ -2562,6 +2593,30 @@ def to_dict(self) -> dict: return result +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class ToolExecutionCompleteFileEdit: + "A file mutation that was actually committed by a built-in file editing tool." + kind: ToolExecutionCompleteFileEditKind + path: str + + @staticmethod + def from_dict(obj: Any) -> "ToolExecutionCompleteFileEdit": + assert isinstance(obj, dict) + kind = parse_enum(ToolExecutionCompleteFileEditKind, obj.get("kind")) + path = from_str(obj.get("path")) + return ToolExecutionCompleteFileEdit( + kind=kind, + path=path, + ) + + def to_dict(self) -> dict: + result: dict = {} + result["kind"] = to_enum(ToolExecutionCompleteFileEditKind, self.kind) + result["path"] = from_str(self.path) + return result + + # Experimental: this type is part of an experimental API and may change or be removed. @dataclass class ToolExecutionCompleteShellExecution: @@ -5462,6 +5517,137 @@ def to_dict(self) -> dict: return result +@dataclass +class HumanResponseRecordedData: + "Durable request-correlated evidence for a typed response to a runtime-owned question or plan review." + actor: HumanResponseActor + request_id: str + response: HumanResponseRecordedResponse + tool_call_id: str | None = None + + @staticmethod + def from_dict(obj: Any) -> "HumanResponseRecordedData": + assert isinstance(obj, dict) + actor = parse_enum(HumanResponseActor, obj.get("actor")) + request_id = from_str(obj.get("requestId")) + response = HumanResponseRecordedResponse.from_dict(obj.get("response")) + tool_call_id = from_union([from_none, from_str], obj.get("toolCallId")) + return HumanResponseRecordedData( + actor=actor, + request_id=request_id, + response=response, + tool_call_id=tool_call_id, + ) + + def to_dict(self) -> dict: + result: dict = {} + result["actor"] = to_enum(HumanResponseActor, self.actor) + result["requestId"] = from_str(self.request_id) + result["response"] = to_class(HumanResponseRecordedResponse, self.response) + if self.tool_call_id is not None: + result["toolCallId"] = from_union([from_none, from_str], self.tool_call_id) + return result + + +@dataclass +class HumanResponseRecordedResponse: + "Exact runtime-owned question or reviewed plan paired with the typed response that settled it." + response_kind: HumanResponseRecordedResponseResponseKind + actions: list[ExitPlanModeAction] | None = None + allow_freeform: bool | None = None + answer: str | None = None + approved: bool | None = None + auto_approve_edits: bool | None = None + choices: list[str] | None = None + content: dict[str, Any] | None = None + feedback: str | None = None + message: str | None = None + plan_content: str | None = None + question: str | None = None + recommended_action: ExitPlanModeAction | None = None + requested_schema: ElicitationRequestedSchema | None = None + selected_action: ExitPlanModeAction | None = None + summary: str | None = None + was_freeform: bool | None = None + + @staticmethod + def from_dict(obj: Any) -> "HumanResponseRecordedResponse": + assert isinstance(obj, dict) + response_kind = parse_enum(HumanResponseRecordedResponseResponseKind, obj.get("responseKind")) + actions = from_union([from_none, lambda x: from_list(lambda x: parse_enum(ExitPlanModeAction, x), x)], obj.get("actions")) + allow_freeform = from_union([from_none, from_bool], obj.get("allowFreeform")) + answer = from_union([from_none, from_str], obj.get("answer")) + approved = from_union([from_none, from_bool], obj.get("approved")) + auto_approve_edits = from_union([from_none, from_bool], obj.get("autoApproveEdits")) + choices = from_union([from_none, lambda x: from_list(from_str, x)], obj.get("choices")) + content = from_union([from_none, lambda x: from_dict(lambda x: x, x)], obj.get("content")) + feedback = from_union([from_none, from_str], obj.get("feedback")) + message = from_union([from_none, from_str], obj.get("message")) + plan_content = from_union([from_none, from_str], obj.get("planContent")) + question = from_union([from_none, from_str], obj.get("question")) + recommended_action = from_union([from_none, lambda x: parse_enum(ExitPlanModeAction, x)], obj.get("recommendedAction")) + requested_schema = from_union([from_none, ElicitationRequestedSchema.from_dict], obj.get("requestedSchema")) + selected_action = from_union([from_none, lambda x: parse_enum(ExitPlanModeAction, x)], obj.get("selectedAction")) + summary = from_union([from_none, from_str], obj.get("summary")) + was_freeform = from_union([from_none, from_bool], obj.get("wasFreeform")) + return HumanResponseRecordedResponse( + response_kind=response_kind, + actions=actions, + allow_freeform=allow_freeform, + answer=answer, + approved=approved, + auto_approve_edits=auto_approve_edits, + choices=choices, + content=content, + feedback=feedback, + message=message, + plan_content=plan_content, + question=question, + recommended_action=recommended_action, + requested_schema=requested_schema, + selected_action=selected_action, + summary=summary, + was_freeform=was_freeform, + ) + + def to_dict(self) -> dict: + result: dict = {} + result["responseKind"] = to_enum(HumanResponseRecordedResponseResponseKind, self.response_kind) + if self.actions is not None: + result["actions"] = from_union([from_none, lambda x: from_list(lambda x: to_enum(ExitPlanModeAction, x), x)], self.actions) + if self.allow_freeform is not None: + result["allowFreeform"] = from_union([from_none, from_bool], self.allow_freeform) + if self.answer is not None: + result["answer"] = from_union([from_none, from_str], self.answer) + if self.approved is not None: + result["approved"] = from_union([from_none, from_bool], self.approved) + if self.auto_approve_edits is not None: + result["autoApproveEdits"] = from_union([from_none, from_bool], self.auto_approve_edits) + if self.choices is not None: + result["choices"] = from_union([from_none, lambda x: from_list(from_str, x)], self.choices) + if self.content is not None: + result["content"] = from_union([from_none, lambda x: from_dict(lambda x: x, x)], self.content) + if self.feedback is not None: + result["feedback"] = from_union([from_none, from_str], self.feedback) + if self.message is not None: + result["message"] = from_union([from_none, from_str], self.message) + if self.plan_content is not None: + result["planContent"] = from_union([from_none, from_str], self.plan_content) + if self.question is not None: + result["question"] = from_union([from_none, from_str], self.question) + if self.recommended_action is not None: + result["recommendedAction"] = from_union([from_none, lambda x: to_enum(ExitPlanModeAction, x)], self.recommended_action) + if self.requested_schema is not None: + result["requestedSchema"] = from_union([from_none, lambda x: to_class(ElicitationRequestedSchema, x)], self.requested_schema) + if self.selected_action is not None: + result["selectedAction"] = from_union([from_none, lambda x: to_enum(ExitPlanModeAction, x)], self.selected_action) + if self.summary is not None: + result["summary"] = from_union([from_none, from_str], self.summary) + if self.was_freeform is not None: + result["wasFreeform"] = from_union([from_none, from_bool], self.was_freeform) + return result + + @dataclass class McpAppToolCallCompleteData: "MCP App view called a tool on a connected MCP server (SEP-1865)" @@ -9631,6 +9817,7 @@ class SessionModelChangeData: previous_reasoning_summary: ReasoningSummary | None = None previous_verbosity: Verbosity | None = None reasoning_effort: str | None = None + reasoning_effort_model: str | None = None reasoning_summary: ReasoningSummary | None = None source: ModelChangeSource | None = None verbosity: Verbosity | None = None @@ -9648,6 +9835,7 @@ def from_dict(obj: Any) -> "SessionModelChangeData": previous_reasoning_summary = from_union([from_none, lambda x: parse_enum(ReasoningSummary, x)], obj.get("previousReasoningSummary")) previous_verbosity = from_union([from_none, lambda x: parse_enum(Verbosity, x)], obj.get("previousVerbosity")) reasoning_effort = from_union([from_none, from_str], obj.get("reasoningEffort")) + reasoning_effort_model = from_union([from_none, from_str], obj.get("reasoningEffortModel")) reasoning_summary = from_union([from_none, lambda x: parse_enum(ReasoningSummary, x)], obj.get("reasoningSummary")) source = from_union([from_none, lambda x: parse_enum(ModelChangeSource, x)], obj.get("source")) verbosity = from_union([from_none, lambda x: parse_enum(Verbosity, x)], obj.get("verbosity")) @@ -9662,6 +9850,7 @@ def from_dict(obj: Any) -> "SessionModelChangeData": previous_reasoning_summary=previous_reasoning_summary, previous_verbosity=previous_verbosity, reasoning_effort=reasoning_effort, + reasoning_effort_model=reasoning_effort_model, reasoning_summary=reasoning_summary, source=source, verbosity=verbosity, @@ -9688,6 +9877,8 @@ def to_dict(self) -> dict: result["previousVerbosity"] = from_union([from_none, lambda x: to_enum(Verbosity, x)], self.previous_verbosity) if self.reasoning_effort is not None: result["reasoningEffort"] = from_union([from_none, from_str], self.reasoning_effort) + if self.reasoning_effort_model is not None: + result["reasoningEffortModel"] = from_union([from_none, from_str], self.reasoning_effort_model) if self.reasoning_summary is not None: result["reasoningSummary"] = from_union([from_none, lambda x: to_enum(ReasoningSummary, x)], self.reasoning_summary) if self.source is not None: @@ -9809,6 +10000,7 @@ class SessionResumeData: continue_pending_work: bool | None = None events_file_size_bytes: int | None = None reasoning_effort: str | None = None + reasoning_effort_model: str | None = None reasoning_summary: ReasoningSummary | None = None remote_steerable: bool | None = None selected_model: str | None = None @@ -9828,6 +10020,7 @@ def from_dict(obj: Any) -> "SessionResumeData": continue_pending_work = from_union([from_none, from_bool], obj.get("continuePendingWork")) events_file_size_bytes = from_union([from_none, from_int], obj.get("eventsFileSizeBytes")) reasoning_effort = from_union([from_none, from_str], obj.get("reasoningEffort")) + reasoning_effort_model = from_union([from_none, from_str], obj.get("reasoningEffortModel")) reasoning_summary = from_union([from_none, lambda x: parse_enum(ReasoningSummary, x)], obj.get("reasoningSummary")) remote_steerable = from_union([from_none, from_bool], obj.get("remoteSteerable")) selected_model = from_union([from_none, from_str], obj.get("selectedModel")) @@ -9844,6 +10037,7 @@ def from_dict(obj: Any) -> "SessionResumeData": continue_pending_work=continue_pending_work, events_file_size_bytes=events_file_size_bytes, reasoning_effort=reasoning_effort, + reasoning_effort_model=reasoning_effort_model, reasoning_summary=reasoning_summary, remote_steerable=remote_steerable, selected_model=selected_model, @@ -9870,6 +10064,8 @@ def to_dict(self) -> dict: result["eventsFileSizeBytes"] = from_union([from_none, to_int], self.events_file_size_bytes) if self.reasoning_effort is not None: result["reasoningEffort"] = from_union([from_none, from_str], self.reasoning_effort) + if self.reasoning_effort_model is not None: + result["reasoningEffortModel"] = from_union([from_none, from_str], self.reasoning_effort_model) if self.reasoning_summary is not None: result["reasoningSummary"] = from_union([from_none, lambda x: to_enum(ReasoningSummary, x)], self.reasoning_summary) if self.remote_steerable is not None: @@ -10163,6 +10359,7 @@ class SessionStartData: detached_from_spawning_parent_session_id: str | None = None github_mcp_tool_config: GitHubMcpToolConfig | None = None reasoning_effort: str | None = None + reasoning_effort_model: str | None = None reasoning_summary: ReasoningSummary | None = None remote_steerable: bool | None = None selected_model: str | None = None @@ -10184,6 +10381,7 @@ def from_dict(obj: Any) -> "SessionStartData": detached_from_spawning_parent_session_id = from_union([from_none, from_str], obj.get("detachedFromSpawningParentSessionId")) github_mcp_tool_config = from_union([from_none, GitHubMcpToolConfig.from_dict], obj.get("githubMcpToolConfig")) reasoning_effort = from_union([from_none, from_str], obj.get("reasoningEffort")) + reasoning_effort_model = from_union([from_none, from_str], obj.get("reasoningEffortModel")) reasoning_summary = from_union([from_none, lambda x: parse_enum(ReasoningSummary, x)], obj.get("reasoningSummary")) remote_steerable = from_union([from_none, from_bool], obj.get("remoteSteerable")) selected_model = from_union([from_none, from_str], obj.get("selectedModel")) @@ -10202,6 +10400,7 @@ def from_dict(obj: Any) -> "SessionStartData": detached_from_spawning_parent_session_id=detached_from_spawning_parent_session_id, github_mcp_tool_config=github_mcp_tool_config, reasoning_effort=reasoning_effort, + reasoning_effort_model=reasoning_effort_model, reasoning_summary=reasoning_summary, remote_steerable=remote_steerable, selected_model=selected_model, @@ -10230,6 +10429,8 @@ def to_dict(self) -> dict: result["githubMcpToolConfig"] = from_union([from_none, lambda x: to_class(GitHubMcpToolConfig, x)], self.github_mcp_tool_config) if self.reasoning_effort is not None: result["reasoningEffort"] = from_union([from_none, from_str], self.reasoning_effort) + if self.reasoning_effort_model is not None: + result["reasoningEffortModel"] = from_union([from_none, from_str], self.reasoning_effort_model) if self.reasoning_summary is not None: result["reasoningSummary"] = from_union([from_none, lambda x: to_enum(ReasoningSummary, x)], self.reasoning_summary) if self.remote_steerable is not None: @@ -11976,6 +12177,8 @@ class ToolExecutionCompleteData: tool_call_id: str error: ToolExecutionCompleteError | None = None # Experimental: this field is part of an experimental API and may change or be removed. + file_edits: list[ToolExecutionCompleteFileEdit] | None = None + # Experimental: this field is part of an experimental API and may change or be removed. fusion: FusionAttribution | None = None interaction_id: str | None = None is_user_requested: bool | None = None @@ -11999,6 +12202,7 @@ def from_dict(obj: Any) -> "ToolExecutionCompleteData": success = from_bool(obj.get("success")) tool_call_id = from_str(obj.get("toolCallId")) error = from_union([from_none, ToolExecutionCompleteError.from_dict], obj.get("error")) + file_edits = from_union([from_none, lambda x: from_list(ToolExecutionCompleteFileEdit.from_dict, x)], obj.get("fileEdits")) fusion = from_union([from_none, FusionAttribution.from_dict], obj.get("fusion")) interaction_id = from_union([from_none, from_str], obj.get("interactionId")) is_user_requested = from_union([from_none, from_bool], obj.get("isUserRequested")) @@ -12016,6 +12220,7 @@ def from_dict(obj: Any) -> "ToolExecutionCompleteData": success=success, tool_call_id=tool_call_id, error=error, + file_edits=file_edits, fusion=fusion, interaction_id=interaction_id, is_user_requested=is_user_requested, @@ -12037,6 +12242,8 @@ def to_dict(self) -> dict: result["toolCallId"] = from_str(self.tool_call_id) if self.error is not None: result["error"] = from_union([from_none, lambda x: to_class(ToolExecutionCompleteError, x)], self.error) + if self.file_edits is not None: + result["fileEdits"] = from_union([from_none, lambda x: from_list(lambda x: to_class(ToolExecutionCompleteFileEdit, x), x)], self.file_edits) if self.fusion is not None: result["fusion"] = from_union([from_none, lambda x: to_class(FusionAttribution, x)], self.fusion) if self.interaction_id is not None: @@ -12437,29 +12644,6 @@ def to_dict(self) -> dict: return {} -@dataclass -class ToolExecutionPartialResultData: - "Streaming tool execution output for incremental result display" - partial_output: str - tool_call_id: str - - @staticmethod - def from_dict(obj: Any) -> "ToolExecutionPartialResultData": - assert isinstance(obj, dict) - partial_output = from_str(obj.get("partialOutput")) - tool_call_id = from_str(obj.get("toolCallId")) - return ToolExecutionPartialResultData( - partial_output=partial_output, - tool_call_id=tool_call_id, - ) - - def to_dict(self) -> dict: - result: dict = {} - result["partialOutput"] = from_str(self.partial_output) - result["toolCallId"] = from_str(self.tool_call_id) - return result - - @dataclass class ToolExecutionProgressData: "Tool execution progress notification with status message" @@ -12715,6 +12899,38 @@ def to_dict(self) -> dict: return result +@dataclass +class ToolShellOutputData: + "Live, append-only shell output. Not persisted or replayed to late subscribers. Text is decoded and redacted per chunk; chunks need not contain complete lines." + sequence: int + text: str + tool_call_id: str + stream: ToolShellOutputStream | None = None + + @staticmethod + def from_dict(obj: Any) -> "ToolShellOutputData": + assert isinstance(obj, dict) + sequence = from_int(obj.get("sequence")) + text = from_str(obj.get("text")) + tool_call_id = from_str(obj.get("toolCallId")) + stream = from_union([from_none, lambda x: parse_enum(ToolShellOutputStream, x)], obj.get("stream")) + return ToolShellOutputData( + sequence=sequence, + text=text, + tool_call_id=tool_call_id, + stream=stream, + ) + + def to_dict(self) -> dict: + result: dict = {} + result["sequence"] = to_int(self.sequence) + result["text"] = from_str(self.text) + result["toolCallId"] = from_str(self.tool_call_id) + if self.stream is not None: + result["stream"] = from_union([from_none, lambda x: to_enum(ToolShellOutputStream, x)], self.stream) + return result + + @dataclass class ToolUserRequestedData: "User-initiated tool invocation request with tool name and arguments" @@ -13543,6 +13759,26 @@ class PermissionRecommendation(Enum): APPROVE = "approve" +# Experimental: this enum is part of an experimental API and may change or be removed. +class ToolExecutionCompleteFileEditKind(Enum): + "Kind of file mutation committed by a built-in editing tool." + # A file was created. + CREATE = "create" + # A file was written by an edit operation. + EDIT = "edit" + # A file was deleted. + DELETE = "delete" + + @classmethod + def _missing_(cls, value: object) -> "ToolExecutionCompleteFileEditKind | None": + if not isinstance(value, str): + return None + member = object.__new__(cls) + member._name_ = "UNKNOWN" + member._value_ = value + return cls._value2member_map_.setdefault(value, member) + + # Experimental: this enum is part of an experimental API and may change or be removed. class UIEphemeralQueryPhase(Enum): "Lifecycle phase for a Rust-owned ephemeral query stream." @@ -13831,6 +14067,23 @@ class HandoffSourceType(Enum): LOCAL = "local" +class HumanResponseActor(Enum): + "Controlled provenance for a typed runtime response. Only `human_response`, minted by a trusted direct-interaction ingress, is human authorization evidence." + # A built-in trusted client submitted the response after direct human interaction. + HUMAN_RESPONSE = "human_response" + # A host or SDK automation submitted the response without direct human interaction. + HOST_AUTOMATION = "host_automation" + # The response came through a legacy or otherwise unattributed ingress. + UNKNOWN = "unknown" + + +class HumanResponseRecordedResponseResponseKind(Enum): + "Exact runtime-owned question or reviewed plan paired with the typed response that settled it. discriminator" + ASK_USER = "ask_user" + USER_INPUT = "user_input" + EXIT_PLAN_MODE = "exit_plan_mode" + + class IndexedSearchDisabledReason(Enum): "Configuration, policy, or workspace condition that disabled indexed search." # Indexed search was explicitly disabled by the environment. @@ -14791,6 +15044,16 @@ class ToolExecutionStartToolDescriptionMetaUIVisibility(Enum): APP = "app" +class ToolShellOutputStream(Enum): + "Shell output source. Terminal output has no separate stdout/stderr attribution." + # Output from the shell command's standard output stream. This is the default when stream is omitted. + STDOUT = "stdout" + # Output from the shell command's standard error stream. + STDERR = "stderr" + # Inherently merged output that cannot be attributed separately to stdout or stderr. + TERMINAL = "terminal" + + class UserMessageAgentMode(Enum): "The agent mode that was active when this message was sent" # The agent is responding interactively to the user. @@ -14861,7 +15124,7 @@ class WorkspaceFileChangedOperation(Enum): UPDATE = "update" -SessionEventData = SessionStartData | SessionResumeData | SessionRemoteSteerableChangedData | SessionErrorData | SessionIdleData | SessionTitleChangedData | SessionScheduleCreatedData | SessionScheduleCancelledData | SessionScheduleRearmedData | SessionAutopilotObjectiveChangedData | SessionInfoData | SessionIndexedSearchData | SessionWarningData | SessionModelChangeData | SessionModelDeselectedData | SessionAutoTierRecommendationData | SessionAutoTierSwitchFailedData | SessionModeChangedData | SessionModeNoticeDeliveredData | SessionSessionLimitsChangedData | SessionPermissionsChangedData | SessionPlanChangedData | SessionTodosChangedData | SessionWorkspaceFileChangedData | SessionHandoffData | SessionTruncationData | SessionSnapshotRewindData | SessionShutdownData | SessionUsageCheckpointData | SessionContextChangedData | SessionUsageInfoData | SessionContextClearedData | SessionCompactionStartData | SessionCompactionCompleteData | SessionTaskCompleteData | SessionCompletionReceiptData | SessionFusionRouteStartedData | SessionFusionRouteFailedData | SessionFusionResolvedData | SessionFusionCompletedData | SessionPermissionRecoveryData | UserMessageData | PendingMessagesModifiedData | AssistantTurnStartData | AssistantTurnRetryData | AgentInterruptedData | AssistantIntentData | AssistantFusionPhaseStartedData | AssistantFusionPhaseActivityData | AssistantFusionPhaseCompletedData | AssistantFusionPhaseFailedData | AssistantServerToolProgressData | AssistantReasoningData | AssistantReasoningDeltaData | AssistantToolCallDeltaData | AssistantStreamingDeltaData | AssistantMessageData | AssistantMessageStartData | AssistantMessageDeltaData | AssistantTurnEndData | AssistantIdleData | AssistantUsageData | PromptCacheBreakData | ModelCallFailureData | ModelCallFinalResultData | ModelCallFinishedData | ModelCallStartData | AbortData | ToolUserRequestedData | ToolExecutionStartData | ToolExecutionPartialResultData | ToolExecutionProgressData | ToolExecutionCompleteData | ToolSearchActivatedData | SkillInvokedData | SkillInvokedRefData | SkillContextDeliveredData | SkillContextDeliveredRefData | SandboxDecisionData | SubagentStartedData | SubagentConfiguredData | SubagentCompletedData | SubagentFailedData | SubagentSelectedData | SubagentDeselectedData | HookStartData | HookEndData | HookProgressData | SessionBinaryAssetData | SystemMessageData | SystemNotificationData | PermissionRequestedData | PermissionCompletedData | PermissionCarriedForwardData | PermissionMessageAuthorizationData | PermissionMessageAuthorizationReadData | PermissionMessageAuthorizationDegradedData | PermissionAssentDetectedData | PermissionContextualAuthorizationData | UserInputRequestedData | UserInputCompletedData | ElicitationRequestedData | ElicitationCompletedData | SamplingRequestedData | SamplingCompletedData | McpOauthRequiredData | McpOauthCompletedData | McpHeadersRefreshRequiredData | McpHeadersRefreshCompletedData | SessionCustomNotificationData | UiEphemeralQueryData | ExternalToolRequestedData | ExternalToolCompletedData | CommandQueuedData | CommandExecuteData | CommandCompletedData | AutoModeSwitchRequestedData | AutoModeSwitchCompletedData | SessionLimitsExhaustedRequestedData | SessionLimitsExhaustedCompletedData | SessionAutoModeResolvedData | SessionManagedSettingsResolvedData | SessionManagedSettingsEnforcedData | CommandsChangedData | CapabilitiesChangedData | ExitPlanModeRequestedData | ExitPlanModeCompletedData | SessionToolsUpdatedData | SessionBackgroundTasksChangedData | WorkflowRunUpdatedData | WorkflowRunStartedData | WorkflowRunSettledData | SessionSkillsLoadedData | SessionCustomAgentsUpdatedData | SessionMcpServersLoadedData | SessionMcpServerStatusChangedData | SessionMcpServerRemovedData | SessionMcpServerNeedsReconnectData | McpToolsListChangedData | McpResourcesListChangedData | McpPromptsListChangedData | SessionExtensionsLoadedData | SessionCanvasOpenedData | SessionCanvasRegistryChangedData | SessionCanvasClosedData | SessionCanvasUnavailableData | SessionCanvasRecordedData | SessionCanvasRemovedData | SessionExtensionsAttachmentsPushedData | McpAppToolCallCompleteData | RawSessionEventData | Data +SessionEventData = SessionStartData | SessionResumeData | SessionRemoteSteerableChangedData | SessionErrorData | SessionIdleData | SessionTitleChangedData | SessionScheduleCreatedData | SessionScheduleCancelledData | SessionScheduleRearmedData | SessionAutopilotObjectiveChangedData | SessionInfoData | SessionIndexedSearchData | SessionWarningData | SessionModelChangeData | SessionModelDeselectedData | SessionAutoTierRecommendationData | SessionAutoTierSwitchFailedData | SessionModeChangedData | SessionModeNoticeDeliveredData | SessionSessionLimitsChangedData | SessionPermissionsChangedData | SessionPlanChangedData | SessionTodosChangedData | SessionWorkspaceFileChangedData | SessionHandoffData | SessionTruncationData | SessionSnapshotRewindData | SessionShutdownData | SessionUsageCheckpointData | SessionContextChangedData | SessionUsageInfoData | SessionContextClearedData | SessionCompactionStartData | SessionCompactionCompleteData | SessionTaskCompleteData | SessionCompletionReceiptData | SessionFusionRouteStartedData | SessionFusionRouteFailedData | SessionFusionResolvedData | SessionFusionCompletedData | SessionPermissionRecoveryData | UserMessageData | PendingMessagesModifiedData | AssistantTurnStartData | AssistantTurnRetryData | AgentInterruptedData | AssistantIntentData | AssistantFusionPhaseStartedData | AssistantFusionPhaseActivityData | AssistantFusionPhaseCompletedData | AssistantFusionPhaseFailedData | AssistantServerToolProgressData | AssistantReasoningData | AssistantReasoningDeltaData | AssistantToolCallDeltaData | AssistantStreamingDeltaData | AssistantMessageData | AssistantMessageStartData | AssistantMessageDeltaData | AssistantTurnEndData | AssistantIdleData | AssistantUsageData | PromptCacheBreakData | ModelCallFailureData | ModelCallFinalResultData | ModelCallFinishedData | ModelCallStartData | AbortData | ToolUserRequestedData | ToolExecutionStartData | ToolExecutionPartialResultData | ToolShellOutputData | ToolExecutionProgressData | ToolExecutionCompleteData | ToolSearchActivatedData | SkillInvokedData | SkillInvokedRefData | SkillContextDeliveredData | SkillContextDeliveredRefData | SandboxDecisionData | SubagentStartedData | SubagentConfiguredData | SubagentCompletedData | SubagentFailedData | SubagentSelectedData | SubagentDeselectedData | HookStartData | HookEndData | HookProgressData | SessionBinaryAssetData | SystemMessageData | SystemNotificationData | PermissionRequestedData | PermissionCompletedData | PermissionCarriedForwardData | PermissionMessageAuthorizationData | PermissionMessageAuthorizationReadData | PermissionMessageAuthorizationDegradedData | PermissionAssentDetectedData | PermissionContextualAuthorizationData | UserInputRequestedData | UserInputCompletedData | ElicitationRequestedData | ElicitationCompletedData | SamplingRequestedData | SamplingCompletedData | McpOauthRequiredData | McpOauthCompletedData | McpHeadersRefreshRequiredData | McpHeadersRefreshCompletedData | SessionCustomNotificationData | UiEphemeralQueryData | ExternalToolRequestedData | ExternalToolCompletedData | CommandQueuedData | CommandExecuteData | CommandCompletedData | AutoModeSwitchRequestedData | AutoModeSwitchCompletedData | SessionLimitsExhaustedRequestedData | SessionLimitsExhaustedCompletedData | SessionAutoModeResolvedData | SessionManagedSettingsResolvedData | SessionManagedSettingsEnforcedData | CommandsChangedData | CapabilitiesChangedData | ExitPlanModeRequestedData | ExitPlanModeCompletedData | HumanResponseRecordedData | SessionToolsUpdatedData | SessionBackgroundTasksChangedData | WorkflowRunUpdatedData | WorkflowRunStartedData | WorkflowRunSettledData | SessionSkillsLoadedData | SessionCustomAgentsUpdatedData | SessionMcpServersLoadedData | SessionMcpServerStatusChangedData | SessionMcpServerRemovedData | SessionMcpServerNeedsReconnectData | McpToolsListChangedData | McpResourcesListChangedData | McpPromptsListChangedData | SessionExtensionsLoadedData | SessionCanvasOpenedData | SessionCanvasRegistryChangedData | SessionCanvasClosedData | SessionCanvasUnavailableData | SessionCanvasRecordedData | SessionCanvasRemovedData | SessionExtensionsAttachmentsPushedData | McpAppToolCallCompleteData | RawSessionEventData | Data @dataclass @@ -14958,6 +15221,7 @@ def from_dict(obj: Any) -> "SessionEvent": case SessionEventType.TOOL_USER_REQUESTED: data = ToolUserRequestedData.from_dict(data_obj) case SessionEventType.TOOL_EXECUTION_START: data = ToolExecutionStartData.from_dict(data_obj) case SessionEventType.TOOL_EXECUTION_PARTIAL_RESULT: data = ToolExecutionPartialResultData.from_dict(data_obj) + case SessionEventType.TOOL_SHELL_OUTPUT: data = ToolShellOutputData.from_dict(data_obj) case SessionEventType.TOOL_EXECUTION_PROGRESS: data = ToolExecutionProgressData.from_dict(data_obj) case SessionEventType.TOOL_EXECUTION_COMPLETE: data = ToolExecutionCompleteData.from_dict(data_obj) case SessionEventType.TOOL_SEARCH_ACTIVATED: data = ToolSearchActivatedData.from_dict(data_obj) @@ -15014,6 +15278,7 @@ def from_dict(obj: Any) -> "SessionEvent": case SessionEventType.CAPABILITIES_CHANGED: data = CapabilitiesChangedData.from_dict(data_obj) case SessionEventType.EXIT_PLAN_MODE_REQUESTED: data = ExitPlanModeRequestedData.from_dict(data_obj) case SessionEventType.EXIT_PLAN_MODE_COMPLETED: data = ExitPlanModeCompletedData.from_dict(data_obj) + case SessionEventType.HUMAN_RESPONSE_RECORDED: data = HumanResponseRecordedData.from_dict(data_obj) case SessionEventType.SESSION_TOOLS_UPDATED: data = SessionToolsUpdatedData.from_dict(data_obj) case SessionEventType.SESSION_BACKGROUND_TASKS_CHANGED: data = SessionBackgroundTasksChangedData.from_dict(data_obj) case SessionEventType.WORKFLOW_RUN_UPDATED: data = WorkflowRunUpdatedData.from_dict(data_obj) @@ -15207,6 +15472,10 @@ def session_event_to_dict(x: SessionEvent) -> Any: "HookEndError", "HookProgressData", "HookStartData", + "HumanResponseActor", + "HumanResponseRecordedData", + "HumanResponseRecordedResponse", + "HumanResponseRecordedResponseResponseKind", "IndexedSearchDisabledReason", "IndexedSearchErrorType", "IndexedSearchIncrementalPhase", @@ -15486,6 +15755,8 @@ def session_event_to_dict(x: SessionEvent) -> Any: "ToolExecutionCompleteContentText", "ToolExecutionCompleteData", "ToolExecutionCompleteError", + "ToolExecutionCompleteFileEdit", + "ToolExecutionCompleteFileEditKind", "ToolExecutionCompleteResult", "ToolExecutionCompleteShellExecution", "ToolExecutionCompleteToolDescription", @@ -15510,6 +15781,8 @@ def session_event_to_dict(x: SessionEvent) -> Any: "ToolExecutionStartToolDescriptionMetaUI", "ToolExecutionStartToolDescriptionMetaUIVisibility", "ToolSearchActivatedData", + "ToolShellOutputData", + "ToolShellOutputStream", "ToolUserRequestedData", "UIEphemeralQueryPhase", "UiEphemeralQueryData", diff --git a/python/copilot/session.py b/python/copilot/session.py index 8faabe2879..91174ce370 100644 --- a/python/copilot/session.py +++ b/python/copilot/session.py @@ -249,6 +249,7 @@ def _consume_task_exception(task: asyncio.Task[Any]) -> None: class SessionFsCapabilities(TypedDict, total=False): sqlite: bool + binary: bool class SessionFsConfig(TypedDict): @@ -416,12 +417,13 @@ class SystemMessageReplaceConfig(TypedDict): "tool_instructions": "Per-tool usage instructions", "custom_instructions": "Repository and organization custom instructions", "runtime_instructions": ( - "Runtime-provided context and instructions" - " (e.g. system notifications, memories, workspace context," - " mode-specific instructions, content-exclusion policy)" + "Runtime-provided system-prompt context and instructions, such as system" + " notifications, memories, workspace context, and content-exclusion policy." + " Mode-specific instructions can travel in transition messages instead." ), "last_instructions": ( - "End-of-prompt instructions: parallel tool calling, persistence, task completion" + "End-of-prompt instructions: parallel tool calling, persistence, task completion," + " and configured subagent-model guidance when the task tool is available" ), } @@ -1226,6 +1228,67 @@ class AgentStopHookOutput(TypedDict, total=False): ] +class SubagentStartHookInput(TypedDict): + """Input when a subagent starts; session metadata belongs to its parent.""" + + sessionId: str + timestamp: datetime + workingDirectory: str + transcriptPath: str + agentName: str + agentDisplayName: NotRequired[str] + agentDescription: NotRequired[str] + + +class SubagentStartHookOutput(TypedDict, total=False): + """Context prepended to the subagent's initial prompt.""" + + additionalContext: str + + +SubagentStartHandler = Callable[ + [SubagentStartHookInput, dict[str, str]], + SubagentStartHookOutput | None | Awaitable[SubagentStartHookOutput | None], +] + + +class SubagentStopHookInput(TypedDict): + """Input after a subagent turn completes; session metadata belongs to its parent.""" + + sessionId: str + timestamp: datetime + workingDirectory: str + transcriptPath: str + agentName: str + agentType: str + stopReason: Literal["end_turn"] + response: str + agentId: NotRequired[str] + agentDisplayName: NotRequired[str] + agentDescription: NotRequired[str] + + +class _SubagentStopBlockHookOutput(TypedDict): + decision: Literal["block"] + reason: str + modifiedResponse: NotRequired[str] + + +class _SubagentStopAllowHookOutput(TypedDict, total=False): + decision: Literal["allow"] + modifiedResponse: str + + +SubagentStopHookOutput = _SubagentStopBlockHookOutput | _SubagentStopAllowHookOutput +"""Block with a nonempty reason, or allow with an optional response rewrite.""" + + +SubagentStopHandler = Callable[ + [SubagentStopHookInput, dict[str, str]], + SubagentStopHookOutput | None | Awaitable[SubagentStopHookOutput | None], +] + + class SessionHooks(TypedDict, total=False): """Configuration for session hooks""" @@ -1239,6 +1302,8 @@ class SessionHooks(TypedDict, total=False): on_session_end: SessionEndHandler on_error_occurred: ErrorOccurredHandler on_agent_stop: AgentStopHandler + on_subagent_start: SubagentStartHandler + on_subagent_stop: SubagentStopHandler # ============================================================================ @@ -3287,6 +3352,8 @@ async def _handle_hooks_invoke(self, hook_type: str, input_data: Any) -> Any: "sessionEnd": hooks.get("on_session_end"), "errorOccurred": hooks.get("on_error_occurred"), "agentStop": hooks.get("on_agent_stop"), + "subagentStart": hooks.get("on_subagent_start"), + "subagentStop": hooks.get("on_subagent_stop"), } handler = handler_map.get(hook_type) diff --git a/python/copilot/session_fs_provider.py b/python/copilot/session_fs_provider.py index c9e90a6442..605d6fe85a 100644 --- a/python/copilot/session_fs_provider.py +++ b/python/copilot/session_fs_provider.py @@ -17,11 +17,12 @@ from __future__ import annotations import abc +import base64 import errno from collections.abc import Sequence from dataclasses import dataclass from datetime import UTC, datetime -from typing import Any +from typing import Any, Protocol, runtime_checkable from .generated.rpc import ( SessionFSError, @@ -31,6 +32,7 @@ SessionFSReaddirResult, SessionFSReaddirWithTypesEntry, SessionFSReaddirWithTypesResult, + SessionFSReadFileBytesResult, SessionFSReadFileResult, SessionFSSqliteExistsResult, SessionFSSqliteQueryType, @@ -48,6 +50,9 @@ SessionFSSqliteTransactionResult as _GeneratedSqliteTransactionResult, ) +_MAX_BINARY_BYTES = (64 * 1024 * 1024 - 1024) // 4 * 3 +_MAX_BINARY_CONTENT_LENGTH = ((_MAX_BINARY_BYTES + 2) // 3) * 4 + @dataclass class SessionFsFileInfo: @@ -60,6 +65,10 @@ class SessionFsFileInfo: birthtime: datetime +class SessionFsWriteFailure(Exception): + """Raise from ``write_file`` only when the failed write changed its target.""" + + class SessionFsProvider(abc.ABC): """Abstract base class for session filesystem providers. @@ -75,7 +84,10 @@ async def read_file(self, path: str) -> str: @abc.abstractmethod async def write_file(self, path: str, content: str, mode: int | None = None) -> None: - """Write *content* to a file, creating parent directories if needed.""" + """Write *content* to a file, creating parent directories if needed. + + Raise :class:`SessionFsWriteFailure` if a failed write changed the target. + """ @abc.abstractmethod async def append_file(self, path: str, content: str, mode: int | None = None) -> None: @@ -110,6 +122,19 @@ async def rename(self, src: str, dest: str) -> None: """Rename / move a file or directory.""" +@runtime_checkable +class SessionFsBinaryProvider(Protocol): + """Optional interface for providers that support exact binary reads and writes.""" + + async def read_file_bytes(self, path: str) -> bytes: + """Read the exact bytes of a file. Raise if it does not exist.""" + ... + + async def write_file_bytes(self, path: str, content: bytes, mode: int | None = None) -> None: + """Write the exact bytes of a file, respecting the optional mode.""" + ... + + class SessionFsSqliteProvider(abc.ABC): """Optional ABC for providers that support SQLite operations. @@ -214,10 +239,42 @@ async def read_file(self, params: Any) -> SessionFSReadFileResult: err = _to_session_fs_error(exc) return SessionFSReadFileResult.from_dict({"content": "", "error": err.to_dict()}) + async def read_file_bytes(self, params: Any) -> SessionFSReadFileBytesResult: + try: + if not isinstance(self._p, SessionFsBinaryProvider): + raise NotImplementedError("Binary reads are not supported by this provider") + data = await self._p.read_file_bytes(params.path) + if len(data) > _MAX_BINARY_BYTES: + raise ValueError("sessionFs.readFileBytes content exceeds the binary read limit") + content = base64.b64encode(data).decode("ascii") + return SessionFSReadFileBytesResult.from_dict({"content": content}) + except Exception as exc: + err = _to_session_fs_error(exc) + return SessionFSReadFileBytesResult.from_dict({"content": "", "error": err.to_dict()}) + async def write_file(self, params: Any) -> SessionFSError | None: try: await self._p.write_file(params.path, params.content, getattr(params, "mode", None)) return None + except Exception as exc: + error = _to_session_fs_error(exc) + if isinstance(exc, SessionFsWriteFailure): + error.write_changed = True + return error + + async def write_file_bytes(self, params: Any) -> SessionFSError | None: + try: + if not isinstance(self._p, SessionFsBinaryProvider): + raise NotImplementedError("Binary writes are not supported by this provider") + if len(params.content) > _MAX_BINARY_CONTENT_LENGTH: + raise ValueError("sessionFs.writeFileBytes content exceeds the binary write limit") + data = base64.b64decode(params.content, validate=True) + if base64.b64encode(data).decode("ascii") != params.content: + raise ValueError("invalid sessionFs.writeFileBytes base64 content") + if len(data) > _MAX_BINARY_BYTES: + raise ValueError("sessionFs.writeFileBytes content exceeds the binary write limit") + await self._p.write_file_bytes(params.path, data, getattr(params, "mode", None)) + return None except Exception as exc: return _to_session_fs_error(exc) diff --git a/python/copilot/tools.py b/python/copilot/tools.py index ad0bcb41bd..efe76a47c3 100644 --- a/python/copilot/tools.py +++ b/python/copilot/tools.py @@ -1,3 +1,5 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. + """ Tool definition utilities for the Copilot SDK. @@ -253,7 +255,7 @@ async def wrapped_handler(invocation: ToolInvocation) -> ToolResult: # Build args based on detected signature call_args = [] if takes_params: - args = invocation.arguments or {} + args = invocation.arguments if invocation.arguments is not None else {} if ptype is not None and _is_pydantic_model(ptype): try: call_args.append(ptype.model_validate(args)) diff --git a/python/e2e/test_compaction_e2e.py b/python/e2e/test_compaction_e2e.py index 73df548839..736ccdb2da 100644 --- a/python/e2e/test_compaction_e2e.py +++ b/python/e2e/test_compaction_e2e.py @@ -1,6 +1,7 @@ """E2E Compaction Tests""" import asyncio +import os import pytest @@ -99,9 +100,27 @@ def _on_compaction_event(event): assert complete_event.data.compaction_tokens_used is not None, ( "Expected compaction tokens-used data" ) - assert (complete_event.data.compaction_tokens_used.input_tokens or 0) > 0, ( - "Expected compaction call to consume input tokens" - ) + # Replay has no prompt usage; recording must preserve the provider's value or absence. + if os.environ.get("GITHUB_ACTIONS") == "true": + assert (complete_event.data.compaction_tokens_used.input_tokens or 0) == 0, ( + "Expected replay compaction input tokens to be zero or unavailable" + ) + else: + exchanges = await ctx.get_exchanges() + compactions = [ + exchange["compactionUsage"] + for exchange in exchanges + if exchange.get("compactionUsage") + and exchange["compactionUsage"]["summary"] == complete_event.data.summary_content + ] + assert len(compactions) == 1, ( + f"Expected one provider compaction chain for the summary, got {compactions!r}" + ) + expected_input_tokens = compactions[0].get("inputTokens") + actual_input_tokens = complete_event.data.compaction_tokens_used.input_tokens + assert actual_input_tokens == expected_input_tokens, ( + f"Expected provider input tokens {expected_input_tokens}, got {actual_input_tokens}" + ) summary = (complete_event.data.summary_content or "").lower() assert "" in summary, "Expected summary to contain " assert "" in summary, "Expected summary to contain " diff --git a/python/e2e/test_pending_work_resume_e2e.py b/python/e2e/test_pending_work_resume_e2e.py index f0ac9c5481..eac29508b2 100644 --- a/python/e2e/test_pending_work_resume_e2e.py +++ b/python/e2e/test_pending_work_resume_e2e.py @@ -18,10 +18,9 @@ from copilot.rpc import ( HandlePendingToolCallRequest, PermissionDecisionRequest, - PermissionDecisionUserNotAvailable, SessionsCheckInUseRequest, ) -from copilot.session import PermissionHandler +from copilot.session import CopilotSession, PermissionHandler, PermissionNoResult from copilot.session_events import ExternalToolRequestedData, PermissionRequestedData from copilot.tools import Tool, ToolInvocation, ToolResult @@ -87,6 +86,26 @@ async def _safe_force_stop(client: CopilotClient) -> None: await client.force_stop() +async def _pending_tool_diagnostics(session: CopilotSession, request_id: str) -> str: + try: + events = await asyncio.wait_for(session.get_events(), PENDING_WORK_TIMEOUT) + except Exception as error: + return f"Rejected request {request_id}; history unavailable: {type(error).__name__}" + # Keep payloads out of failure reports while preserving request/turn ordering. + recent = [ + { + "type": event.type.value, + "id": event.id, + "ephemeral": event.ephemeral, + "request_id": getattr(event.data, "request_id", None), + "tool_call_id": getattr(event.data, "tool_call_id", None), + "success": getattr(event.data, "success", None), + } + for event in events[-32:] + ] + return f"Rejected request {request_id}; session {session.session_id}; recent events: {recent}" + + class TestPendingWorkResume: async def test_should_continue_pending_permission_request_after_resume( self, ctx: E2ETestContext @@ -145,7 +164,8 @@ def resumed_tool_handler(args): try: session2 = await resumed_client.resume_session( session_id, - on_permission_request=lambda req, inv: PermissionDecisionUserNotAvailable(), + # The manual approval below owns this response. + on_permission_request=lambda req, inv: PermissionNoResult(), continue_pending_work=True, tools=[_make_pending_tool("resume_permission_tool", resumed_tool_handler)], ) @@ -169,7 +189,7 @@ def resumed_tool_handler(args): permission_event_task.cancel() await asyncio.gather(permission_event_task, return_exceptions=True) if not release_original.done(): - release_original.set_result(PermissionDecisionUserNotAvailable()) + release_original.set_result(PermissionNoResult()) finally: await _safe_force_stop(server) @@ -340,14 +360,18 @@ async def tool_b(args): result="PARALLEL_B_BETA", ) ) - assert result_b.success + assert result_b.success, await _pending_tool_diagnostics( + session2, tool_b_event.data.request_id + ) result_a = await session2.rpc.tools.handle_pending_tool_call( HandlePendingToolCallRequest( request_id=tool_a_event.data.request_id, result="PARALLEL_A_ALPHA", ) ) - assert result_a.success + assert result_a.success, await _pending_tool_diagnostics( + session2, tool_a_event.data.request_id + ) await session2.disconnect() finally: diff --git a/python/e2e/test_session_fs_e2e.py b/python/e2e/test_session_fs_e2e.py index 035db125ba..73104db314 100644 --- a/python/e2e/test_session_fs_e2e.py +++ b/python/e2e/test_session_fs_e2e.py @@ -3,6 +3,7 @@ from __future__ import annotations import asyncio +import base64 import datetime as dt import os import re @@ -27,7 +28,10 @@ SessionCompactionCompleteData, SessionEvent, ) -from copilot.session_fs_provider import SessionFsFileInfo, SessionFsProvider +from copilot.session_fs_provider import ( + SessionFsFileInfo, + SessionFsProvider, +) from .testharness import DEFAULT_GITHUB_TOKEN, E2ETestContext @@ -66,6 +70,51 @@ async def session_fs_client(ctx: E2ETestContext): class TestSessionFs: + async def test_should_view_an_image_that_exists_only_in_the_binary_session_fs_provider( + self, ctx: E2ETestContext + ): + image_path = "/sdk-provider-image.png" + image_bytes = base64.b64decode( + "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8DwHwAFBQIAX8jx0gAAAABJRU5ErkJggg==" + ) + provider_root = Path(ctx.work_dir) / "binary-provider" + read_paths: list[str] = [] + client = CopilotClient( + connection=RuntimeConnection.for_stdio(path=ctx.cli_path), + working_directory=ctx.work_dir, + env=ctx.get_env(), + github_token=DEFAULT_GITHUB_TOKEN, + session_fs={**SESSION_FS_CONFIG, "capabilities": {"binary": True}}, + ) + try: + session = await client.create_session( + on_permission_request=PermissionHandler.approve_all, + create_session_fs_handler=lambda session: _BinaryTestSessionFsProvider( + provider_root, session.session_id, read_paths + ), + ) + stored_path = provider_path(provider_root, session.session_id, image_path) + stored_path.parent.mkdir(parents=True, exist_ok=True) + stored_path.write_bytes(image_bytes) + assert not Path(image_path).exists() + + msg = await session.send_and_wait( + "Use the view tool to view /sdk-provider-image.png, " + "then reply with exactly SDK_PROVIDER_IMAGE_DONE." + ) + assert msg is not None and "SDK_PROVIDER_IMAGE_DONE" in (msg.data.content or "") + assert image_path in read_paths + events = await session.get_events() + assert any( + event.type.value == "session.binary_asset" + and event.data.mime_type == "image/png" + and event.data.data == base64.b64encode(image_bytes).decode("ascii") + for event in events + ) + await session.disconnect() + finally: + await client.stop() + async def test_should_route_file_operations_through_the_session_fs_provider( self, ctx: E2ETestContext, session_fs_client: CopilotClient ): @@ -638,6 +687,21 @@ async def rename(self, src: str, dest: str) -> None: self._path(src).replace(d) +class _BinaryTestSessionFsProvider(_TestSessionFsProvider): + def __init__(self, provider_root: Path, session_id: str, read_paths: list[str]): + super().__init__(provider_root, session_id) + self._read_paths = read_paths + + async def read_file_bytes(self, path: str) -> bytes: + self._read_paths.append(path) + return self._path(path).read_bytes() + + async def write_file_bytes(self, path: str, content: bytes, mode: int | None = None) -> None: + target = self._path(path) + target.parent.mkdir(parents=True, exist_ok=True) + target.write_bytes(content) + + def create_test_session_fs_handler(provider_root: Path): def create_handler(session): return _TestSessionFsProvider(provider_root, session.session_id) diff --git a/python/e2e/test_subagent_hooks_e2e.py b/python/e2e/test_subagent_hooks_e2e.py index 145d3f68bd..e06168f0ce 100644 --- a/python/e2e/test_subagent_hooks_e2e.py +++ b/python/e2e/test_subagent_hooks_e2e.py @@ -1,18 +1,28 @@ """ -Tests for sub-agent hooks functionality — verifies preToolUse/postToolUse hooks -fire for tool calls made by sub-agents spawned via the task tool. +Tests for sub-agent hooks — verifies preToolUse/postToolUse and +subagentStart/subagentStop for sub-agents spawned via the task tool. """ from __future__ import annotations import asyncio +import json import os +import sys +from datetime import UTC, datetime from uuid import uuid4 import httpx import pytest -from copilot import CopilotRequestContext, CopilotRequestHandler +from copilot import ( + CopilotRequestContext, + CopilotRequestHandler, + SubagentStartHookInput, + SubagentStartHookOutput, + SubagentStopHookInput, + SubagentStopHookOutput, +) from copilot.client import CopilotClient, RuntimeConnection from copilot.session import PermissionHandler @@ -21,10 +31,15 @@ pytestmark = pytest.mark.asyncio(loop_scope="module") +CHILD_CONTEXT = "Subagent start hook verified: read the requested file." +STOP_RESPONSE_PREFIX = "Subagent stop hook verified: " + class _RecordingRequestHandler(CopilotRequestHandler): def __init__(self) -> None: self.records: list[dict[str, str | None]] = [] + self.child_prompts: list[str] = [] + self.modified_parent_request = asyncio.Event() async def send_request( self, request: httpx.Request, ctx: CopilotRequestContext @@ -37,6 +52,26 @@ async def send_request( "interaction_type": ctx.interaction_type, } ) + if _is_inference_url(str(request.url)): + body = json.loads(request.content) + if ctx.parent_agent_id: + for message in body.get("messages", []): + if message["role"] != "user": + continue + content = message.get("content") + parts = [content] if isinstance(content, str) else content + if isinstance(parts, list): + for part in parts: + if isinstance(part, str): + text = part + elif isinstance(part, dict): + text = part.get("text") + else: + continue + if isinstance(text, str) and f"{CHILD_CONTEXT}\n\n" in text: + self.child_prompts.append(text) + elif STOP_RESPONSE_PREFIX in json.dumps(body): + self.modified_parent_request.set() return await super().send_request(request, ctx) @@ -66,11 +101,12 @@ def _assert_subagent_request_metadata(records: list[dict[str, str | None]]) -> N class TestSubagentHooks: - async def test_should_invoke_pretooluse_and_posttooluse_hooks_for_sub_agent_tool_calls( - self, ctx: E2ETestContext - ): - """Test that preToolUse/postToolUse hooks fire for sub-agent tool calls""" + async def test_should_apply_subagent_lifecycle_hook_outputs(self, ctx: E2ETestContext): + """Test tool and lifecycle hooks for a real subagent under the replay proxy.""" hook_log = [] + start_calls: list[tuple[SubagentStartHookInput, dict[str, str]]] = [] + stop_calls: list[tuple[SubagentStopHookInput, dict[str, str]]] = [] + subagent_stopped = asyncio.Event() request_handler = _RecordingRequestHandler() waiting_text = ( "I've launched an explore agent to read subagent-test.txt. " @@ -110,6 +146,19 @@ async def on_post_tool_use(input_data, invocation): await parent_waiting.wait() return None + async def on_subagent_start( + input_data: SubagentStartHookInput, invocation: dict[str, str] + ) -> SubagentStartHookOutput: + start_calls.append((input_data, invocation)) + return {"additionalContext": CHILD_CONTEXT} + + async def on_subagent_stop( + input_data: SubagentStopHookInput, invocation: dict[str, str] + ) -> SubagentStopHookOutput: + stop_calls.append((input_data, invocation)) + subagent_stopped.set() + return {"modifiedResponse": f"{STOP_RESPONSE_PREFIX}{input_data['response']}"} + # Create a client with the session-based subagents feature flag env = ctx.get_env() env["COPILOT_EXP_COPILOT_CLI_SESSION_BASED_SUBAGENTS"] = "true" @@ -124,62 +173,101 @@ async def on_post_tool_use(input_data, invocation): request_handler=request_handler, ) - session = await client.create_session( - session_id=parent_session_id, - on_permission_request=PermissionHandler.approve_all, - hooks={ - "on_pre_tool_use": on_pre_tool_use, - "on_post_tool_use": on_post_tool_use, - }, - ) + try: + session = await client.create_session( + session_id=parent_session_id, + on_permission_request=PermissionHandler.approve_all, + hooks={ + "on_pre_tool_use": on_pre_tool_use, + "on_post_tool_use": on_post_tool_use, + "on_subagent_start": on_subagent_start, + "on_subagent_stop": on_subagent_stop, + }, + ) + try: + write_file(ctx.work_dir, "subagent-test.txt", "Hello from subagent test!") - # Create a file for the sub-agent to read - write_file(ctx.work_dir, "subagent-test.txt", "Hello from subagent test!") + def on_event(event): + if ( + not event.agent_id + and event.type.value == "assistant.message" + and event.data.content == waiting_text + ): + parent_waiting.set() - def on_event(event): - if ( - not event.agent_id - and event.type.value == "assistant.message" - and event.data.content == waiting_text - ): - parent_waiting.set() + unsubscribe = session.on(on_event) + try: + response = await session.send_and_wait( + "Use the task tool to spawn an explore agent that reads the file " + "subagent-test.txt in the current directory and reports its contents. " + "You must use the task tool." + ) + assert response is not None and not response.agent_id + assert response.data.content == final_text + replies = [ + event.data.content + for event in await session.get_events() + if not event.agent_id + and event.type.value == "assistant.message" + and event.data.content in (waiting_text, final_text) + ] + assert replies == [waiting_text, final_text] + await asyncio.wait_for(subagent_stopped.wait(), 120) + finally: + parent_waiting.set() + unsubscribe() - unsubscribe = session.on(on_event) - try: - response = await session.send_and_wait( - "Use the task tool to spawn an explore agent that reads the file " - "subagent-test.txt in the current directory and reports its contents. " - "You must use the task tool." - ) - assert response is not None and not response.agent_id - assert response.data.content == final_text - replies = [ - event.data.content - for event in await session.get_events() - if not event.agent_id - and event.type.value == "assistant.message" - and event.data.content in (waiting_text, final_text) - ] - assert replies == [waiting_text, final_text] - finally: - parent_waiting.set() - unsubscribe() - - # Parent tool hooks fire for "task" - task_pre = [h for h in hook_log if h["kind"] == "pre" and h["toolName"] == "task"] - assert len(task_pre) >= 1, "preToolUse should fire for the parent's 'task' tool call" - - # Sub-agent tool hooks fire for "view" - view_pre = [h for h in hook_log if h["kind"] == "pre" and h["toolName"] == "view"] - view_post = [h for h in hook_log if h["kind"] == "post" and h["toolName"] == "view"] - assert len(view_pre) > 0, "preToolUse should fire for the sub-agent's 'view' tool call" - assert len(view_post) > 0, "postToolUse should fire for the sub-agent's 'view' tool call" - - # input.session_id distinguishes parent from sub-agent - assert view_pre[0]["sessionId"] != task_pre[0]["sessionId"], ( - "Sub-agent tool hooks should have a different sessionId than parent tool hooks" - ) - _assert_subagent_request_metadata(request_handler.records) + assert len(start_calls) == 1, f"unexpected subagent starts: {start_calls}" + assert stop_calls, "subagentStop should fire after the explore agent finishes" + started, start_invocation = start_calls[0] + stopped, stop_invocation = stop_calls[-1] + assert start_invocation == stop_invocation == {"session_id": session.session_id} + assert started["sessionId"] == stopped["sessionId"] == session.session_id + assert stopped["timestamp"] >= started["timestamp"] + assert started["agentName"] == stopped["agentName"] == "explore" + assert stopped["agentType"] == "explore" + assert stopped["agentId"] + assert stopped["stopReason"] == "end_turn" + assert "Hello from subagent test!" in stopped["response"] + for input_data in (started, stopped): + assert isinstance(input_data["timestamp"], datetime) + assert input_data["timestamp"].tzinfo == UTC + assert os.path.normcase( + os.path.normpath(input_data["workingDirectory"]) + ) == os.path.normcase(os.path.normpath(ctx.work_dir)) + assert isinstance(input_data["transcriptPath"], str) + assert input_data.get("agentDisplayName") is None + assert input_data.get("agentDescription") is None + + task_pre = [h for h in hook_log if h["kind"] == "pre" and h["toolName"] == "task"] + assert task_pre, "preToolUse should fire for the parent's 'task' tool call" - await session.disconnect() - await client.stop() + view_pre = [h for h in hook_log if h["kind"] == "pre" and h["toolName"] == "view"] + view_post = [h for h in hook_log if h["kind"] == "post" and h["toolName"] == "view"] + assert view_pre, "preToolUse should fire for the sub-agent's 'view' tool call" + assert view_post, "postToolUse should fire for the sub-agent's 'view' tool call" + assert view_pre[0]["sessionId"] != task_pre[0]["sessionId"], ( + "Sub-agent tool hooks should have a different sessionId than parent tool hooks" + ) + _assert_subagent_request_metadata(request_handler.records) + assert any( + f'{CHILD_CONTEXT}\n\nRead the file "subagent-test.txt"' in prompt + for prompt in request_handler.child_prompts + ), "start context must be prepended to the child's model prompt" + await asyncio.wait_for(request_handler.modified_parent_request.wait(), 120) + finally: + first_error = sys.exception() + try: + await session.disconnect() + except BaseException as cleanup_error: + if first_error is None: + raise + first_error.add_note(f"session.disconnect failed: {cleanup_error!r}") + finally: + first_error = sys.exception() + try: + await client.stop() + except BaseException as cleanup_error: + if first_error is None: + raise + first_error.add_note(f"client.stop failed: {cleanup_error!r}") diff --git a/python/e2e/test_tools_e2e.py b/python/e2e/test_tools_e2e.py index 303ddc3ac9..fe45ea3dba 100644 --- a/python/e2e/test_tools_e2e.py +++ b/python/e2e/test_tools_e2e.py @@ -1,9 +1,11 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. + """E2E Tools Tests""" import os import pytest -from pydantic import BaseModel, Field +from pydantic import BaseModel, Field, RootModel from copilot import ToolSet, define_tool from copilot.rpc import ( @@ -19,6 +21,31 @@ class TestTools: + async def test_string_schema_apply_patch_override_binds_patch_input(self, ctx: E2ETestContext): + patch = "*** Begin Patch\n*** Add File: override-marker.txt\n+from-native\n*** End Patch" + calls = [] + + @define_tool( + "apply_patch", + description="Apply a patch", + overrides_built_in_tool=True, + ) + def apply_patch(params: RootModel[str], invocation: ToolInvocation) -> str: + calls.append((params.root, invocation.arguments)) + return "HOST_PATCH_HANDLED" + + session = await ctx.client.create_session( + on_permission_request=PermissionHandler.approve_all, tools=[apply_patch] + ) + response = await session.send_and_wait( + "Use apply_patch to apply the supplied patch.", timeout=10.0 + ) + + assert calls == [(patch, patch)] + assert response is not None + assert response.data.content == "Host override completed." + assert not os.path.exists(os.path.join(ctx.work_dir, "override-marker.txt")) + async def test_invokes_built_in_tools(self, ctx: E2ETestContext): readme_path = os.path.join(ctx.work_dir, "README.md") with open(readme_path, "w") as f: diff --git a/python/test_cli_download.py b/python/test_cli_download.py index a3d1d95e1d..34e6f32a4d 100644 --- a/python/test_cli_download.py +++ b/python/test_cli_download.py @@ -29,6 +29,7 @@ def _release_package(runtime_platform: str) -> bytes: f"package/ripgrep/bin/{runtime_platform}/rg": b"ripgrep", "package/definitions/future.json": b"{}", "package/app.js": b"excluded", + "package/cli-main.js": b"excluded", "package/LICENSE.md": b"excluded", } buffer = io.BytesIO() @@ -190,6 +191,7 @@ def test_cli_and_runtime_share_one_staged_bundle(tmp_path, monkeypatch, version) assert (install_dir / "ripgrep" / "bin" / runtime_platform / "rg").read_bytes() == b"ripgrep" assert (install_dir / "definitions" / "future.json").read_bytes() == b"{}" assert not (install_dir / "app.js").exists() + assert not (install_dir / "cli-main.js").exists() assert (install_dir / ".hostless-runtime-assets-v2").is_file() assert fetch_mock.call_count == 2 if os.name != "nt": diff --git a/python/test_client.py b/python/test_client.py index e93bce0ca3..57d284dd45 100644 --- a/python/test_client.py +++ b/python/test_client.py @@ -8,6 +8,8 @@ import inspect import json import os +import threading +from dataclasses import fields from datetime import UTC, datetime from tempfile import TemporaryDirectory from unittest.mock import AsyncMock, Mock, patch @@ -46,6 +48,9 @@ DiagnosticsConfiguration, DiagnosticSourcesConfiguration, MCPDiagnosticSourceConfiguration, + SessionFSReadFileBytesRequest, + SessionFSSetProviderCapabilities, + SessionFSWriteFileBytesRequest, ) from copilot.session import CopilotSession, PermissionHandler from copilot.session_events import ( @@ -56,10 +61,23 @@ SessionEvent, SessionEventType, ) +from copilot.session_fs_provider import SessionFsProvider, create_session_fs_adapter from copilot.tools import Tool from e2e.testharness import CLI_PATH +def test_session_fs_capabilities_preserve_positional_sqlite(): + capabilities = SessionFSSetProviderCapabilities(True) + + assert capabilities.sqlite is True + assert capabilities.binary is None + assert [field.name for field in fields(capabilities)] == ["sqlite", "binary"] + assert capabilities.to_dict() == {"sqlite": True} + assert SessionFSSetProviderCapabilities.from_dict( + {"sqlite": True, "binary": False} + ) == SessionFSSetProviderCapabilities(True, binary=False) + + def test_inprocess_connection_has_no_child_process_options(): connection = RuntimeConnection.for_inprocess() @@ -785,6 +803,167 @@ async def mock_request(method, params, **kwargs): finally: await client.force_stop() + @pytest.mark.asyncio + @pytest.mark.parametrize("cleanup_failure", [False, True]) + async def test_cloud_create_deletes_session_on_binary_provider_validation_failure( + self, cleanup_failure, caplog + ): + client = CopilotClient( + connection=RuntimeConnection.for_uri("localhost:1234"), + session_fs={ + "initial_working_directory": "/", + "session_state_path": "/session-state", + "conventions": "posix", + "capabilities": {"binary": True}, + }, + ) + requests = [] + + async def mock_request(method, params, **kwargs): + requests.append((method, params)) + if method == "session.create": + result = {"sessionId": "server-assigned-session"} + kwargs["on_response_inline"](result) + return result + if cleanup_failure: + return {"success": False, "error": "cleanup rejected"} + return {"success": True} + + client._client = Mock(request=mock_request) + with pytest.raises(ValueError, match="does not implement SessionFsBinaryProvider"): + await client.create_session( + cloud=CloudSessionOptions( + repository=CloudSessionRepository( + owner="github", name="copilot-sdk", branch="main" + ) + ), + create_session_fs_handler=lambda _: Mock(spec=SessionFsProvider), + ) + + assert "sessionId" not in requests[0][1] + assert requests[-1] == ("session.delete", {"sessionId": "server-assigned-session"}) + assert "server-assigned-session" not in client._sessions + if cleanup_failure: + assert "Failed to delete cloud session" in caplog.text + + @pytest.mark.asyncio + @pytest.mark.parametrize("valid_provider", [False, True]) + async def test_cloud_create_cancellation_waits_for_provider_validation(self, valid_provider): + client = CopilotClient( + connection=RuntimeConnection.for_uri("localhost:1234"), + session_fs={ + "initial_working_directory": "/", + "session_state_path": "/session-state", + "conventions": "posix", + "capabilities": {"binary": True}, + }, + ) + loop = asyncio.get_running_loop() + provider_started = asyncio.Event() + callback_finished = asyncio.Event() + delete_called = asyncio.Event() + release_provider = threading.Event() + requests = [] + + async def mock_request(method, params, **kwargs): + requests.append((method, params)) + if method == "session.create": + result = {"sessionId": "server-assigned-session"} + + def invoke_callback(): + try: + kwargs["on_response_inline"](result) + finally: + loop.call_soon_threadsafe(callback_finished.set) + + await asyncio.to_thread(invoke_callback) + return result + if method == "session.delete": + delete_called.set() + return {"success": True} + return {} + + def create_provider(_): + loop.call_soon_threadsafe(provider_started.set) + release_provider.wait() + provider = Mock(spec=SessionFsProvider) + if valid_provider: + provider.read_file_bytes = AsyncMock(return_value=b"image") + provider.write_file_bytes = AsyncMock() + return provider + + client._client = Mock(request=mock_request) + creation = asyncio.create_task( + client.create_session( + cloud=CloudSessionOptions( + repository=CloudSessionRepository( + owner="github", name="copilot-sdk", branch="main" + ) + ), + create_session_fs_handler=create_provider, + ) + ) + try: + await asyncio.wait_for(provider_started.wait(), timeout=5) + creation.cancel() + with pytest.raises(asyncio.CancelledError): + await asyncio.wait_for(creation, timeout=5) + assert not delete_called.is_set() + + release_provider.set() + await asyncio.wait_for(callback_finished.wait(), timeout=5) + if valid_provider: + assert not delete_called.is_set() + else: + await asyncio.wait_for(delete_called.wait(), timeout=5) + assert requests[-1] == ( + "session.delete", + {"sessionId": "server-assigned-session"}, + ) + assert "server-assigned-session" not in client._sessions + finally: + release_provider.set() + if not creation.done(): + creation.cancel() + await asyncio.gather(creation, return_exceptions=True) + + @pytest.mark.asyncio + async def test_cloud_create_preserves_session_when_interest_registration_fails(self): + client = CopilotClient(connection=RuntimeConnection.for_uri("localhost:1234")) + requests = [] + + async def mock_request(method, params, **kwargs): + requests.append((method, params)) + if method == "session.create": + result = {"sessionId": "server-assigned-session"} + kwargs["on_response_inline"](result) + return result + if method == "session.eventLog.registerInterest": + raise RuntimeError("interest registration failed") + return {} + + client._client = Mock(request=mock_request) + with pytest.raises(RuntimeError, match="interest registration failed"): + await client.create_session( + cloud=CloudSessionOptions( + repository=CloudSessionRepository( + owner="github", name="copilot-sdk", branch="main" + ) + ), + on_mcp_auth_request=lambda request: {"kind": "cancelled"}, + ) + + assert "sessionId" not in requests[0][1] + assert ( + "session.eventLog.registerInterest", + { + "sessionId": "server-assigned-session", + "eventType": "mcp.oauth_required", + }, + ) in requests + assert not any(method == "session.delete" for method, _ in requests) + assert "server-assigned-session" not in client._sessions + @pytest.mark.asyncio async def test_create_and_resume_session_forward_github_mcp_tool_config(self): client = CopilotClient(connection=RuntimeConnection.for_stdio(path=CLI_PATH)) @@ -1694,6 +1873,46 @@ async def test_connect_via_tcp_uses_family_independent_resolution(self): class TestSessionFsConfig: + @pytest.mark.asyncio + async def test_binary_write_preserves_bytes_and_rejects_invalid_base64(self): + provider = Mock(spec=SessionFsProvider) + provider.read_file_bytes = AsyncMock(return_value=b"\x00\xff") + provider.write_file_bytes = AsyncMock() + adapter = create_session_fs_adapter(provider) + request = SessionFSWriteFileBytesRequest( + session_id="session", path="/image.png", content="AP/+AQ==", mode=0o600 + ) + assert await adapter.write_file_bytes(request) is None + provider.write_file_bytes.assert_awaited_once_with("/image.png", b"\x00\xff\xfe\x01", 0o600) + for content in ("AA==AAAA", "AA==\n", "A" * (64 * 1024 * 1024)): + error = await adapter.write_file_bytes( + SessionFSWriteFileBytesRequest( + session_id="session", path="/image.png", content=content + ) + ) + assert error is not None + assert error.code.value == "UNKNOWN" + provider.write_file_bytes.assert_awaited_once() + + @pytest.mark.asyncio + async def test_binary_read_rejects_oversized_provider_bytes(self): + provider = Mock(spec=SessionFsProvider) + provider.read_file_bytes = AsyncMock( + return_value=bytes((64 * 1024 * 1024 - 1024) // 4 * 3 + 1) + ) + provider.write_file_bytes = AsyncMock() + result = await create_session_fs_adapter(provider).read_file_bytes( + SessionFSReadFileBytesRequest(session_id="session", path="/image.png") + ) + + provider.read_file_bytes.assert_awaited_once_with("/image.png") + assert result.content == "" + assert result.error is not None + assert result.error.code.value == "UNKNOWN" + assert ( + result.error.message == "sessionFs.readFileBytes content exceeds the binary read limit" + ) + def test_missing_initial_cwd(self): with pytest.raises(ValueError, match="session_fs.initial_working_directory is required"): CopilotClient( diff --git a/python/test_event_forward_compatibility.py b/python/test_event_forward_compatibility.py index eeb1170f53..401eec6359 100644 --- a/python/test_event_forward_compatibility.py +++ b/python/test_event_forward_compatibility.py @@ -30,6 +30,7 @@ SessionResumeData, SessionStartData, SessionTaskCompleteData, + ToolExecutionCompleteData, UserMessageAgentMode, session_event_from_dict, session_event_to_dict, @@ -39,6 +40,30 @@ class TestEventForwardCompatibility: """Test forward compatibility for unknown event types.""" + def test_future_file_edit_kind_preserves_tool_completion(self): + wire = { + "id": str(uuid4()), + "timestamp": datetime.now().isoformat(), + "parentId": None, + "type": "tool.execution_complete", + "data": { + "toolCallId": "call-1", + "success": True, + "fileEdits": [{"path": "/future.txt", "kind": "append"}], + }, + } + + event = session_event_from_dict(wire) + assert isinstance(event.data, ToolExecutionCompleteData) + assert event.data.file_edits is not None + assert event.data.file_edits[0].kind.name == "UNKNOWN" + assert event.data.file_edits[0].kind.value == "append" + assert session_event_to_dict(event)["data"]["fileEdits"] == wire["data"]["fileEdits"] + other = session_event_from_dict(wire) + assert event.data.file_edits[0].kind is other.data.file_edits[0].kind + assert event.data.file_edits[0] == other.data.file_edits[0] + assert len({event.data.file_edits[0].kind, other.data.file_edits[0].kind}) == 1 + @pytest.mark.parametrize( ("event_type", "data"), [("session.idle", {}), ("user.message", {"content": "hello"})] ) diff --git a/python/test_rpc_generated.py b/python/test_rpc_generated.py index cf736be5fa..df5dc87232 100644 --- a/python/test_rpc_generated.py +++ b/python/test_rpc_generated.py @@ -7,7 +7,13 @@ import pytest from copilot._jsonrpc import JsonRpcClient -from copilot.generated.rpc import ClientGlobalApiHandlers, register_client_global_api_handlers +from copilot.generated.rpc import ( + ClientGlobalApiHandlers, + CustomizationReloadOutcome, + CustomizationReloadStatus, + CustomizationReloadSubsystem, + register_client_global_api_handlers, +) from copilot.rpc import ( BuiltinToolInputSchemaType, CommandsApi, @@ -31,6 +37,25 @@ ) +def test_customization_reload_outcome_preserves_future_enum_values(): + payload = {"status": "newStatus", "subsystem": "newSubsystem", "detail": "new component"} + outcome = CustomizationReloadOutcome.from_dict(payload) + + assert outcome.status.value == payload["status"] + assert outcome.subsystem.value == payload["subsystem"] + assert outcome.to_dict() == payload + assert outcome.status is CustomizationReloadStatus("newStatus") + assert outcome.subsystem is CustomizationReloadSubsystem("newSubsystem") + assert CustomizationReloadOutcome.from_dict(payload).status is outcome.status + assert CustomizationReloadOutcome.from_dict(payload).subsystem is outcome.subsystem + assert CustomizationReloadStatus("anotherStatus") is not outcome.status + assert CustomizationReloadSubsystem("anotherSubsystem") is not outcome.subsystem + assert CustomizationReloadStatus("reloaded") is CustomizationReloadStatus.RELOADED + assert CustomizationReloadSubsystem("skills") is CustomizationReloadSubsystem.SKILLS + with pytest.raises(ValueError): + CustomizationReloadStatus(None) + + async def test_host_dispose_deserializes_empty_acknowledgement(): client = Mock(request=AsyncMock(return_value={})) diff --git a/python/test_session_fs_provider.py b/python/test_session_fs_provider.py new file mode 100644 index 0000000000..462abd6b9e --- /dev/null +++ b/python/test_session_fs_provider.py @@ -0,0 +1,41 @@ +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# -------------------------------------------------------------------------------------------- + +"""Unit tests for the session filesystem provider adapter.""" + +from types import SimpleNamespace +from unittest.mock import AsyncMock, Mock + +import pytest + +from copilot import SessionFsWriteFailure +from copilot.session_fs_provider import SessionFsProvider, create_session_fs_adapter + + +@pytest.mark.parametrize( + ("failure", "changed"), + [(RuntimeError("rejected"), None), (SessionFsWriteFailure("disk full"), True)], +) +async def test_failed_write_reports_only_known_changes(failure: Exception, changed: bool | None): + provider = Mock(spec=SessionFsProvider) + provider.write_file = AsyncMock(side_effect=failure) + adapter = create_session_fs_adapter(provider) + + error = await adapter.write_file(SimpleNamespace(path="/file", content="data", mode=None)) + + assert error is not None + assert error.message == str(failure) + assert error.write_changed is changed + assert error.to_dict().get("writeChanged") is changed + + +async def test_write_marker_does_not_leak_into_other_methods(): + provider = Mock(spec=SessionFsProvider) + provider.read_file = AsyncMock(side_effect=SessionFsWriteFailure("read failed")) + adapter = create_session_fs_adapter(provider) + + result = await adapter.read_file(SimpleNamespace(path="/file")) + + assert result.error is not None + assert result.error.write_changed is None diff --git a/python/test_subagent_hooks.py b/python/test_subagent_hooks.py new file mode 100644 index 0000000000..71db1c6c00 --- /dev/null +++ b/python/test_subagent_hooks.py @@ -0,0 +1,130 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. + +from datetime import UTC, datetime + +import pytest + +from copilot import ( + SessionHooks, + SubagentStartHookInput, + SubagentStartHookOutput, + SubagentStopHookInput, + SubagentStopHookOutput, +) +from copilot.session import CopilotSession + + +@pytest.mark.asyncio +async def test_subagent_start_dispatches_normalized_input_and_context(): + captured: list[tuple[SubagentStartHookInput, dict[str, str]]] = [] + + async def on_start( + input_data: SubagentStartHookInput, invocation: dict[str, str] + ) -> SubagentStartHookOutput: + captured.append((input_data, invocation)) + return {"additionalContext": "Report the entire file"} + + hooks: SessionHooks = {"on_subagent_start": on_start} + session = CopilotSession("parent", None) + session._register_hooks(hooks) + + output = await session._handle_hooks_invoke( + "subagentStart", + { + "sessionId": "parent", + "timestamp": 1_700_000_000_000, + "cwd": "work", + "transcriptPath": "transcript.jsonl", + "agentName": "explore", + "agentDisplayName": "Explorer", + "agentDescription": "Reads files", + }, + ) + + assert output == {"additionalContext": "Report the entire file"} + assert captured == [ + ( + { + "sessionId": "parent", + "timestamp": datetime.fromtimestamp(1_700_000_000, tz=UTC), + "workingDirectory": "work", + "transcriptPath": "transcript.jsonl", + "agentName": "explore", + "agentDisplayName": "Explorer", + "agentDescription": "Reads files", + }, + {"session_id": "parent"}, + ) + ] + + +@pytest.mark.asyncio +@pytest.mark.parametrize( + "hook_output", + [ + {"decision": "block", "reason": "Read the rest of the file"}, + {"modifiedResponse": "Verified: Hello from subagent test!"}, + {"decision": "allow", "modifiedResponse": "Verified: Hello from subagent test!"}, + ], +) +async def test_subagent_stop_dispatches_normalized_input_and_verdict( + hook_output: SubagentStopHookOutput, +): + captured: list[tuple[SubagentStopHookInput, dict[str, str]]] = [] + + def on_stop( + input_data: SubagentStopHookInput, invocation: dict[str, str] + ) -> SubagentStopHookOutput: + captured.append((input_data, invocation)) + return hook_output + + hooks: SessionHooks = {"on_subagent_stop": on_stop} + session = CopilotSession("parent", None) + session._register_hooks(hooks) + + output = await session._handle_hooks_invoke( + "subagentStop", + { + "sessionId": "parent", + "timestamp": 1_700_000_000_000, + "cwd": "work", + "transcriptPath": "transcript.jsonl", + "agentName": "explore", + "agentType": "explore", + "stopReason": "end_turn", + "response": "Hello from subagent test!", + }, + ) + + assert output == hook_output + assert captured == [ + ( + { + "sessionId": "parent", + "timestamp": datetime.fromtimestamp(1_700_000_000, tz=UTC), + "workingDirectory": "work", + "transcriptPath": "transcript.jsonl", + "agentName": "explore", + "agentType": "explore", + "stopReason": "end_turn", + "response": "Hello from subagent test!", + }, + {"session_id": "parent"}, + ) + ] + + +@pytest.mark.asyncio +async def test_subagent_stop_without_handler_returns_none(): + session = CopilotSession("parent", None) + session._register_hooks({"on_subagent_start": lambda _input, _invocation: None}) + + assert await session._handle_hooks_invoke("subagentStop", {}) is None + + +@pytest.mark.asyncio +async def test_subagent_start_without_handler_returns_none(): + session = CopilotSession("parent", None) + session._register_hooks({"on_subagent_stop": lambda _input, _invocation: None}) + + assert await session._handle_hooks_invoke("subagentStart", {}) is None diff --git a/python/test_tools.py b/python/test_tools.py index 031f68c053..5d8f6043dd 100644 --- a/python/test_tools.py +++ b/python/test_tools.py @@ -1,9 +1,11 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. + """Unit tests for define_tool""" import json import pytest -from pydantic import BaseModel, ConfigDict, Field, field_validator +from pydantic import BaseModel, ConfigDict, Field, RootModel, field_validator from copilot import define_tool from copilot.generated.rpc import ExternalToolTextResultForLlm @@ -18,6 +20,27 @@ class TestDefineTool: + @pytest.mark.parametrize("patch", ["", "*** Begin Patch\n*** End Patch"]) + async def test_string_schema_handler_preserves_scalar_arguments(self, patch: str): + calls = [] + + @define_tool("apply_patch", description="Apply a patch", overrides_built_in_tool=True) + def apply_patch(params: RootModel[str], invocation: ToolInvocation) -> str: + calls.append((params.root, invocation.arguments)) + return "accepted" + + result = await apply_patch.handler( + ToolInvocation( + session_id="session-1", + tool_call_id="call-1", + tool_name="apply_patch", + arguments=patch, + ) + ) + + assert result.result_type == "success", result.error + assert calls == [(patch, patch)] + def test_creates_tool_with_correct_name_and_description(self): class Params(BaseModel): query: str diff --git a/rust/AGENTS.md b/rust/AGENTS.md index a7509e57d4..de40a114f8 100644 --- a/rust/AGENTS.md +++ b/rust/AGENTS.md @@ -100,9 +100,13 @@ cargo check --no-default-features --features local-runtime Run it with `COPILOT_SKIP_CLI_DOWNLOAD` unset. Tests that start the runtime must also set `COPILOT_CLI_PATH` to the prepared same-checkout wrapper. -Follow the SDK's existing crate-local unit-test and `tests/` integration-test -layout. Integration tests needing SDK test helpers use the `test-support` -feature because the library is compiled without `cfg(test)`. +Keep unit tests in external test-only modules: `foo.rs` declares +`#[cfg(test)] mod tests;`, and its tests live in `foo/tests.rs`. Crate roots +such as `lib.rs` use a sibling `tests.rs`; descriptive module names keep their +corresponding file names. Start each test-only file with `#![cfg(test)]`. +Keep integration tests in `tests/`. Integration tests needing SDK test helpers +use the `test-support` feature because the library is compiled without +`cfg(test)`. Default features bundle the CLI; disabling them changes what must be provided externally. For transport-specific E2E settings, bundled-runtime checks, and diff --git a/rust/README.md b/rust/README.md index 6e91639175..7b3d28cde6 100644 --- a/rust/README.md +++ b/rust/README.md @@ -783,12 +783,14 @@ let session = client .await?; ``` -**Hook events:** `PreToolUse`, `PostToolUse`, `PostToolUseFailure`, `UserPromptSubmitted`, `UserPromptTransformed`, `SessionStart`, `SessionEnd`, `ErrorOccurred`. Each carries typed input/output structs. `PostToolUse` only fires on success; override `on_post_tool_use_failure` to observe failed tool calls. Return `HookOutput::None` for events you don't handle. +**Hook events:** `PreToolUse`, `PostToolUse`, `PostToolUseFailure`, `UserPromptSubmitted`, `UserPromptTransformed`, `SessionStart`, `SessionEnd`, `ErrorOccurred`, `AgentStop`, `SubagentStart`, and `SubagentStop`. Each carries typed input/output structs. `PostToolUse` only fires on success; override `on_post_tool_use_failure` to observe failed tool calls. `on_subagent_start` can inject `additional_context` into the child agent's initial prompt; `on_subagent_stop` can return a `decision` of `"block"` with a `reason` to continue the child or a `modified_response` to replace its final answer. Return `HookOutput::None` for events you don't handle. ### System Message Transforms Transforms customize system message sections during session creation. The SDK injects `action: "transform"` entries for each section ID your transform handles. +`last_instructions` includes configured subagent-model guidance when the `task` tool is available. Removing or replacing this section in customize mode also removes that guidance; a `SystemMessageTransform` handling this section receives its complete content, including the guidance, and any replacement it returns is authoritative. Append, prepend, and preserve retain their usual section semantics. These overrides change prompt prose only, not configured subagent models, tool availability, or runtime dispatch policy. `runtime_instructions` is a separate section: removing it does not remove `last_instructions`. + ```rust,ignore use github_copilot_sdk::transforms::*; use async_trait::async_trait; @@ -905,6 +907,34 @@ session.set_tools(vec![search_tool, filter_tool]).await?; The agent sees the new tools from its next model request. The new handlers take effect as soon as the runtime accepts the replacement, and calls already running finish on the handlers that started them. If the runtime rejects the replacement, nothing changes. A model request already in flight was made with the previous tools, so the agent can still call a tool you removed; this session won't answer that call, so replace tools while the session is idle if a running turn might still call a tool you remove. See [Changing tools during a session](../docs/features/changing-tools.md) for the behavior shared by all SDKs. +#### String-schema `apply_patch` overrides + +With the `derive` feature enabled, an explicit `apply_patch` override can use +`define_tool::` to declare a root string schema. The model sees a +required `input` property, but the runtime restores the scalar patch text before +dispatch. The typed handler receives a `String`, not an `{"input": ...}` object. +This example returns trimmed patch text; replace the handler body with your own +patch implementation: + +```rust,ignore +use github_copilot_sdk::tool::define_tool; +use github_copilot_sdk::ToolResult; + +let apply_patch = define_tool::( + "apply_patch", + "Apply a patch", + |_invocation, patch| async move { + Ok(ToolResult::Text(patch.trim().to_owned())) + }, +) +.with_overrides_built_in_tool(true); + +let config = config.with_tools(vec![apply_patch]); +``` + +String-schema `apply_patch` overrides cannot contain JSON Schema references; +use an object schema if references are needed. + ### Permission Policies Set a permission policy directly on `SessionConfig` with the chainable builders. They install a synthesized `PermissionHandler` so only permission requests are intercepted; every other event flows through unchanged. @@ -1339,6 +1369,16 @@ let session = client See [`examples/session_fs.rs`](examples/session_fs.rs) for a complete in-memory provider implementation. +To let the `view` tool read images stored only in that provider, enable +`SessionFsCapabilities::new().with_binary(true)` with +`SessionFsConfig::with_capabilities`, implement `SessionFsBinaryProvider` +(`read_file_bytes` and `write_file_bytes`), and return it from +`SessionFsProvider::binary`. A registered provider +without binary support does not fall back to a local file with the same +path. + +Binary reads and writes are limited to 50,330,880 raw bytes (approximately 48 MiB); +larger results return a filesystem error before encoding or decoding. - **Canvas action dispatch is a single trait method, not per-action closures.** The Node SDK binds an optional `handler` closure on each entry of a canvas's diff --git a/rust/build/in_process.rs b/rust/build/in_process.rs index 18be31f127..cfae1539ca 100644 --- a/rust/build/in_process.rs +++ b/rust/build/in_process.rs @@ -320,6 +320,7 @@ fn hostless_runtime_path(source: &str, platform: Platform) -> Option { "app.js", "assets", "changelog.json", + "cli-main.js", "foundry-local-sdk", "index.js", "LICENSE.md", diff --git a/rust/src/cache_paths.rs b/rust/src/cache_paths.rs index 116e235be1..61c63993c2 100644 --- a/rust/src/cache_paths.rs +++ b/rust/src/cache_paths.rs @@ -43,51 +43,5 @@ fn version_component(version: &str) -> String { } #[cfg(test)] -mod tests { - use std::ffi::OsStr; - use std::fs; - use std::path::{Path, PathBuf}; - - use tempfile::tempdir; - - use super::{CLI_CACHE_DIR, cache_install_dir, runtime_install_dir}; - - #[test] - fn custom_runtime_directory_is_used_directly() { - let cache = Path::new("ignored-cache"); - let custom = OsStr::new("custom-runtime"); - - assert_eq!( - runtime_install_dir(Some(custom), cache, "1.2.3"), - PathBuf::from(custom) - ); - } - - #[test] - fn stale_runtime_cleanup_cannot_remove_same_version_bundled_cli() { - let cache = tempdir().expect("create cache root"); - let version = "1.2.3/test"; - let bundled_cli_dir = cache_install_dir(cache.path(), CLI_CACHE_DIR, version); - let runtime_dir = runtime_install_dir(None, cache.path(), version); - let bundled_cli = bundled_cli_dir.join(if cfg!(windows) { - "copilot.exe" - } else { - "copilot" - }); - let runtime_marker = runtime_dir.join(".hostless-runtime-assets-v1"); - - fs::create_dir_all(&bundled_cli_dir).expect("create bundled CLI directory"); - fs::write(&bundled_cli, b"bundled-cli").expect("write bundled CLI"); - fs::create_dir_all(&runtime_dir).expect("create runtime directory"); - fs::write(&runtime_marker, b"stale").expect("write stale runtime marker"); - - assert_ne!(runtime_dir, bundled_cli_dir); - fs::remove_dir_all(&runtime_dir).expect("clear stale runtime directory"); - - assert_eq!( - fs::read(&bundled_cli).expect("bundled CLI survives runtime cleanup"), - b"bundled-cli" - ); - assert!(!runtime_marker.exists()); - } -} +#[path = "cache_paths/tests.rs"] +mod tests; diff --git a/rust/src/cache_paths/tests.rs b/rust/src/cache_paths/tests.rs new file mode 100644 index 0000000000..3e165d40c4 --- /dev/null +++ b/rust/src/cache_paths/tests.rs @@ -0,0 +1,52 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +#![cfg(test)] + +use std::ffi::OsStr; +use std::fs; +use std::path::{Path, PathBuf}; + +use tempfile::tempdir; + +use super::{CLI_CACHE_DIR, cache_install_dir, runtime_install_dir}; + +#[test] +fn custom_runtime_directory_is_used_directly() { + let cache = Path::new("ignored-cache"); + let custom = OsStr::new("custom-runtime"); + + assert_eq!( + runtime_install_dir(Some(custom), cache, "1.2.3"), + PathBuf::from(custom) + ); +} + +#[test] +fn stale_runtime_cleanup_cannot_remove_same_version_bundled_cli() { + let cache = tempdir().expect("create cache root"); + let version = "1.2.3/test"; + let bundled_cli_dir = cache_install_dir(cache.path(), CLI_CACHE_DIR, version); + let runtime_dir = runtime_install_dir(None, cache.path(), version); + let bundled_cli = bundled_cli_dir.join(if cfg!(windows) { + "copilot.exe" + } else { + "copilot" + }); + let runtime_marker = runtime_dir.join(".hostless-runtime-assets-v1"); + + fs::create_dir_all(&bundled_cli_dir).expect("create bundled CLI directory"); + fs::write(&bundled_cli, b"bundled-cli").expect("write bundled CLI"); + fs::create_dir_all(&runtime_dir).expect("create runtime directory"); + fs::write(&runtime_marker, b"stale").expect("write stale runtime marker"); + + assert_ne!(runtime_dir, bundled_cli_dir); + fs::remove_dir_all(&runtime_dir).expect("clear stale runtime directory"); + + assert_eq!( + fs::read(&bundled_cli).expect("bundled CLI survives runtime cleanup"), + b"bundled-cli" + ); + assert!(!runtime_marker.exists()); +} diff --git a/rust/src/canvas.rs b/rust/src/canvas.rs index ddb92a11e6..5768f00112 100644 --- a/rust/src/canvas.rs +++ b/rust/src/canvas.rs @@ -154,6 +154,12 @@ pub trait CanvasHandler: Send + Sync { ) -> CanvasResult; /// Handle a non-lifecycle action declared by the canvas. + /// + /// The returned value is sent to the model as the `invoke_canvas_action` + /// tool result. To return text and images, as tool handlers do, serialize a + /// [`ToolResultExpanded`](crate::types::ToolResultExpanded) (with + /// `binary_results_for_llm`) into the returned value; any other value is + /// rendered to the model as JSON text. async fn on_action( &self, _ctx: crate::generated::api_types::CanvasProviderInvokeActionRequest, @@ -171,130 +177,4 @@ pub trait CanvasHandler: Send + Sync { } #[cfg(test)] -mod tests { - use serde_json::json; - - use super::*; - use crate::generated::api_types::{ - CanvasProviderInvokeActionRequest, CanvasProviderOpenRequest, CanvasProviderOpenResult, - }; - use crate::types::SessionId; - - struct EchoHandler; - - #[async_trait] - impl CanvasHandler for EchoHandler { - async fn on_open( - &self, - ctx: CanvasProviderOpenRequest, - ) -> CanvasResult { - Ok(CanvasProviderOpenResult { - url: Some(format!("https://example.test/{}", ctx.canvas_id)), - title: Some("Echo".to_string()), - status: Some("ready".to_string()), - }) - } - - async fn on_action(&self, ctx: CanvasProviderInvokeActionRequest) -> CanvasResult { - Ok(json!({ "echoed": ctx.action_name, "input": ctx.input })) - } - } - - #[test] - fn declaration_serializes_camel_case_and_skips_none() { - let decl = CanvasDeclaration { - id: "counter".to_string(), - display_name: "Counter".to_string(), - description: "Count things".to_string(), - input_schema: None, - actions: Some(vec![CanvasAction { - name: "increment".to_string(), - description: Some("bump".to_string()), - input_schema: None, - }]), - }; - - let value = serde_json::to_value(&decl).unwrap(); - - assert_eq!(value["id"], "counter"); - assert_eq!(value["displayName"], "Counter"); - assert_eq!(value["description"], "Count things"); - assert_eq!(value["actions"][0]["name"], "increment"); - } - - #[tokio::test] - async fn handler_on_open_returns_response() { - let handler = EchoHandler; - let response = handler - .on_open(CanvasProviderOpenRequest { - session_id: SessionId::from("s1"), - extension_id: "project:echo".to_string(), - canvas_id: "echo".to_string(), - instance_id: "echo-1".to_string(), - input: Some(json!({ "x": 1 })), - host: None, - session: None, - }) - .await - .unwrap(); - - assert_eq!(response.url.as_deref(), Some("https://example.test/echo")); - assert_eq!(response.title.as_deref(), Some("Echo")); - assert_eq!(response.status.as_deref(), Some("ready")); - } - - #[tokio::test] - async fn handler_on_action_returns_value() { - let handler = EchoHandler; - let result = handler - .on_action(CanvasProviderInvokeActionRequest { - session_id: SessionId::from("s1"), - extension_id: "project:echo".to_string(), - canvas_id: "echo".to_string(), - instance_id: "inst-1".to_string(), - action_name: "shout".to_string(), - input: Some(json!("hi")), - host: None, - session: None, - }) - .await - .unwrap(); - - assert_eq!(result["echoed"], "shout"); - assert_eq!(result["input"], "hi"); - } - - #[tokio::test] - async fn default_on_action_returns_no_handler_error() { - struct OpenOnly; - #[async_trait] - impl CanvasHandler for OpenOnly { - async fn on_open( - &self, - _ctx: CanvasProviderOpenRequest, - ) -> CanvasResult { - Ok(CanvasProviderOpenResult { - url: None, - title: None, - status: None, - }) - } - } - - let err = OpenOnly - .on_action(CanvasProviderInvokeActionRequest { - session_id: SessionId::from("s1"), - extension_id: "project:open-only".to_string(), - canvas_id: "x".to_string(), - instance_id: "x-1".to_string(), - action_name: "anything".to_string(), - input: Some(Value::Null), - host: None, - session: None, - }) - .await - .unwrap_err(); - - assert_eq!(err.code, "canvas_action_no_handler"); - } -} +mod tests; diff --git a/rust/src/canvas/tests.rs b/rust/src/canvas/tests.rs new file mode 100644 index 0000000000..339f1439b9 --- /dev/null +++ b/rust/src/canvas/tests.rs @@ -0,0 +1,131 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +#![cfg(test)] + +use serde_json::json; + +use super::*; +use crate::generated::api_types::{ + CanvasProviderInvokeActionRequest, CanvasProviderOpenRequest, CanvasProviderOpenResult, +}; +use crate::types::SessionId; + +struct EchoHandler; + +#[async_trait] +impl CanvasHandler for EchoHandler { + async fn on_open( + &self, + ctx: CanvasProviderOpenRequest, + ) -> CanvasResult { + Ok(CanvasProviderOpenResult { + url: Some(format!("https://example.test/{}", ctx.canvas_id)), + title: Some("Echo".to_string()), + status: Some("ready".to_string()), + }) + } + + async fn on_action(&self, ctx: CanvasProviderInvokeActionRequest) -> CanvasResult { + Ok(json!({ "echoed": ctx.action_name, "input": ctx.input })) + } +} + +#[test] +fn declaration_serializes_camel_case_and_skips_none() { + let decl = CanvasDeclaration { + id: "counter".to_string(), + display_name: "Counter".to_string(), + description: "Count things".to_string(), + input_schema: None, + actions: Some(vec![CanvasAction { + name: "increment".to_string(), + description: Some("bump".to_string()), + input_schema: None, + }]), + }; + + let value = serde_json::to_value(&decl).unwrap(); + + assert_eq!(value["id"], "counter"); + assert_eq!(value["displayName"], "Counter"); + assert_eq!(value["description"], "Count things"); + assert_eq!(value["actions"][0]["name"], "increment"); +} + +#[tokio::test] +async fn handler_on_open_returns_response() { + let handler = EchoHandler; + let response = handler + .on_open(CanvasProviderOpenRequest { + session_id: SessionId::from("s1"), + extension_id: "project:echo".to_string(), + canvas_id: "echo".to_string(), + instance_id: "echo-1".to_string(), + input: Some(json!({ "x": 1 })), + host: None, + session: None, + }) + .await + .unwrap(); + + assert_eq!(response.url.as_deref(), Some("https://example.test/echo")); + assert_eq!(response.title.as_deref(), Some("Echo")); + assert_eq!(response.status.as_deref(), Some("ready")); +} + +#[tokio::test] +async fn handler_on_action_returns_value() { + let handler = EchoHandler; + let result = handler + .on_action(CanvasProviderInvokeActionRequest { + session_id: SessionId::from("s1"), + extension_id: "project:echo".to_string(), + canvas_id: "echo".to_string(), + instance_id: "inst-1".to_string(), + action_name: "shout".to_string(), + input: Some(json!("hi")), + host: None, + session: None, + }) + .await + .unwrap(); + + assert_eq!(result["echoed"], "shout"); + assert_eq!(result["input"], "hi"); +} + +#[tokio::test] +async fn default_on_action_returns_no_handler_error() { + struct OpenOnly; + #[async_trait] + impl CanvasHandler for OpenOnly { + async fn on_open( + &self, + _ctx: CanvasProviderOpenRequest, + ) -> CanvasResult { + Ok(CanvasProviderOpenResult { + url: None, + title: None, + status: None, + }) + } + } + + let err = OpenOnly + .on_action(CanvasProviderInvokeActionRequest { + session_id: SessionId::from("s1"), + extension_id: "project:open-only".to_string(), + canvas_id: "x".to_string(), + instance_id: "x-1".to_string(), + action_name: "anything".to_string(), + input: Some(Value::Null), + host: None, + session: None, + }) + .await + .unwrap_err(); + + assert_eq!(err.code, "canvas_action_no_handler"); +} diff --git a/rust/src/copilot_request_handler/http_response_reader.rs b/rust/src/copilot_request_handler/http_response_reader.rs index 96a5946a2a..d8bffad9b2 100644 --- a/rust/src/copilot_request_handler/http_response_reader.rs +++ b/rust/src/copilot_request_handler/http_response_reader.rs @@ -92,180 +92,4 @@ impl HttpResponseReader { } #[cfg(test)] -mod tests { - use std::sync::Arc; - use std::sync::atomic::{AtomicUsize, Ordering}; - use std::time::Duration; - - use futures_util::stream; - - use super::*; - - #[tokio::test] - async fn preserves_empty_large_fragmented_and_partial_bodies() { - for size in [0, 1, CHUNK_SIZE - 1, CHUNK_SIZE, CHUNK_SIZE * 3 + 7] { - let expected: Vec = (0..size).map(|i| (i % 256) as u8).collect(); - for fragment_size in [1, 1024, CHUNK_SIZE, CHUNK_SIZE * 4] { - let fragments: Vec<_> = expected - .chunks(fragment_size) - .map(Bytes::copy_from_slice) - .collect(); - let body = stream::iter( - [Bytes::new()] - .into_iter() - .chain(fragments) - .chain([Bytes::new()]) - .map(Ok), - ); - let mut reader = HttpResponseReader::new(Box::pin(body)); - let mut output = Vec::new(); - let mut actual = Vec::new(); - while reader.next_chunk(&mut output).await.unwrap() { - assert!(output.len() <= CHUNK_SIZE); - actual.extend_from_slice(&output); - while reader.can_read() { - reader.read_more().await; - } - assert!(reader.buffered.capacity() <= CHUNK_SIZE); - assert!(output.capacity() <= CHUNK_SIZE); - } - assert_eq!(actual, expected); - } - } - } - - #[tokio::test] - async fn flushes_partial_bytes_without_polling_pending_input() { - let body = stream::once(async { Ok(Bytes::from_static(b"data: first\n\n")) }) - .chain(stream::pending()); - let mut reader = HttpResponseReader::new(Box::pin(body)); - let mut output = Vec::new(); - assert!( - reader - .next_chunk(&mut output) - .now_or_never() - .unwrap() - .unwrap() - ); - assert_eq!(output, b"data: first\n\n"); - assert!(reader.read_more().now_or_never().is_none()); - } - - #[tokio::test] - async fn bounds_read_ahead_even_for_single_byte_fragments() { - let polls = Arc::new(AtomicUsize::new(0)); - let counter = polls.clone(); - let body = stream::repeat_with(move || { - counter.fetch_add(1, Ordering::SeqCst); - Ok(Bytes::from_static(b"x")) - }); - let mut reader = HttpResponseReader::new(Box::pin(body)); - let mut output = Vec::new(); - assert!(reader.next_chunk(&mut output).await.unwrap()); - while reader.can_read() { - reader.read_more().await; - } - assert_eq!(polls.load(Ordering::SeqCst), CHUNK_SIZE + 1); - assert_eq!(reader.buffered.len(), CHUNK_SIZE); - assert_eq!(reader.buffered.capacity(), CHUNK_SIZE); - assert_eq!(output.capacity(), CHUNK_SIZE); - assert!(reader.pending.is_empty()); - } - - struct BackingAllocation { - data: Vec, - visible: usize, - live: Arc, - } - - impl AsRef<[u8]> for BackingAllocation { - fn as_ref(&self) -> &[u8] { - &self.data[..self.visible] - } - } - - impl Drop for BackingAllocation { - fn drop(&mut self) { - self.live.fetch_sub(1, Ordering::SeqCst); - } - } - - #[tokio::test] - async fn releases_large_backing_allocations_including_empty_frames() { - let live = Arc::new(AtomicUsize::new(0)); - let counter = live.clone(); - let body = stream::iter([0, 1, 1, CHUNK_SIZE * 2]).map(move |visible| { - counter.fetch_add(1, Ordering::SeqCst); - Ok(Bytes::from_owner(BackingAllocation { - data: vec![42; CHUNK_SIZE * 64], - visible, - live: counter.clone(), - })) - }); - let mut reader = HttpResponseReader::new(Box::pin(body)); - reader.read_more().await; - assert_eq!(live.load(Ordering::SeqCst), 0); - reader.read_more().await; - reader.read_more().await; - assert_eq!(live.load(Ordering::SeqCst), 0); - reader.read_more().await; - assert_eq!(live.load(Ordering::SeqCst), 1); - assert_eq!(reader.pending.len(), CHUNK_SIZE + 2); - let mut output = Vec::new(); - assert!(reader.next_chunk(&mut output).await.unwrap()); - reader.read_more().await; - assert_eq!(live.load(Ordering::SeqCst), 1); - assert!(reader.next_chunk(&mut output).await.unwrap()); - reader.read_more().await; - assert_eq!(live.load(Ordering::SeqCst), 0); - } - - #[tokio::test] - async fn flushes_last_partial_before_upstream_error_or_panic() { - for panic in [false, true] { - let body = stream::iter([Ok(Bytes::from_static(b"partial"))]).chain(stream::once( - async move { - assert!(!panic, "failing user stream"); - Err(CopilotRequestError::message("upstream failed")) - }, - )); - let mut reader = HttpResponseReader::new(Box::pin(body)); - reader.read_more().await; - reader.read_more().await; - let mut output = Vec::new(); - assert!(reader.next_chunk(&mut output).await.unwrap()); - assert_eq!(output, b"partial"); - let error = reader.next_chunk(&mut output).await.unwrap_err(); - assert_eq!( - error.to_string(), - if panic { - "HTTP response body stream panicked" - } else { - "upstream failed" - } - ); - } - } - - #[tokio::test] - async fn dropping_reader_drops_pending_source() { - let (tx, rx) = tokio::sync::mpsc::channel(1); - let mut reader = - HttpResponseReader::new(Box::pin(tokio_stream::wrappers::ReceiverStream::new(rx))); - assert!(reader.read_more().now_or_never().is_none()); - drop(reader); - assert!(tx.is_closed()); - } - - #[tokio::test] - async fn always_ready_empty_frames_yield_to_cancellation() { - let mut reader = - HttpResponseReader::new(Box::pin(stream::repeat_with(|| Ok(Bytes::new())))); - let mut output = Vec::new(); - assert!( - tokio::time::timeout(Duration::from_millis(10), reader.next_chunk(&mut output)) - .await - .is_err() - ); - } -} +mod tests; diff --git a/rust/src/copilot_request_handler/http_response_reader/tests.rs b/rust/src/copilot_request_handler/http_response_reader/tests.rs new file mode 100644 index 0000000000..45988726a4 --- /dev/null +++ b/rust/src/copilot_request_handler/http_response_reader/tests.rs @@ -0,0 +1,179 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +#![cfg(test)] + +use std::sync::Arc; +use std::sync::atomic::{AtomicUsize, Ordering}; +use std::time::Duration; + +use futures_util::stream; + +use super::*; + +#[tokio::test] +async fn preserves_empty_large_fragmented_and_partial_bodies() { + for size in [0, 1, CHUNK_SIZE - 1, CHUNK_SIZE, CHUNK_SIZE * 3 + 7] { + let expected: Vec = (0..size).map(|i| (i % 256) as u8).collect(); + for fragment_size in [1, 1024, CHUNK_SIZE, CHUNK_SIZE * 4] { + let fragments: Vec<_> = expected + .chunks(fragment_size) + .map(Bytes::copy_from_slice) + .collect(); + let body = stream::iter( + [Bytes::new()] + .into_iter() + .chain(fragments) + .chain([Bytes::new()]) + .map(Ok), + ); + let mut reader = HttpResponseReader::new(Box::pin(body)); + let mut output = Vec::new(); + let mut actual = Vec::new(); + while reader.next_chunk(&mut output).await.unwrap() { + assert!(output.len() <= CHUNK_SIZE); + actual.extend_from_slice(&output); + while reader.can_read() { + reader.read_more().await; + } + assert!(reader.buffered.capacity() <= CHUNK_SIZE); + assert!(output.capacity() <= CHUNK_SIZE); + } + assert_eq!(actual, expected); + } + } +} + +#[tokio::test] +async fn flushes_partial_bytes_without_polling_pending_input() { + let body = + stream::once(async { Ok(Bytes::from_static(b"data: first\n\n")) }).chain(stream::pending()); + let mut reader = HttpResponseReader::new(Box::pin(body)); + let mut output = Vec::new(); + assert!( + reader + .next_chunk(&mut output) + .now_or_never() + .unwrap() + .unwrap() + ); + assert_eq!(output, b"data: first\n\n"); + assert!(reader.read_more().now_or_never().is_none()); +} + +#[tokio::test] +async fn bounds_read_ahead_even_for_single_byte_fragments() { + let polls = Arc::new(AtomicUsize::new(0)); + let counter = polls.clone(); + let body = stream::repeat_with(move || { + counter.fetch_add(1, Ordering::SeqCst); + Ok(Bytes::from_static(b"x")) + }); + let mut reader = HttpResponseReader::new(Box::pin(body)); + let mut output = Vec::new(); + assert!(reader.next_chunk(&mut output).await.unwrap()); + while reader.can_read() { + reader.read_more().await; + } + assert_eq!(polls.load(Ordering::SeqCst), CHUNK_SIZE + 1); + assert_eq!(reader.buffered.len(), CHUNK_SIZE); + assert_eq!(reader.buffered.capacity(), CHUNK_SIZE); + assert_eq!(output.capacity(), CHUNK_SIZE); + assert!(reader.pending.is_empty()); +} + +struct BackingAllocation { + data: Vec, + visible: usize, + live: Arc, +} + +impl AsRef<[u8]> for BackingAllocation { + fn as_ref(&self) -> &[u8] { + &self.data[..self.visible] + } +} + +impl Drop for BackingAllocation { + fn drop(&mut self) { + self.live.fetch_sub(1, Ordering::SeqCst); + } +} + +#[tokio::test] +async fn releases_large_backing_allocations_including_empty_frames() { + let live = Arc::new(AtomicUsize::new(0)); + let counter = live.clone(); + let body = stream::iter([0, 1, 1, CHUNK_SIZE * 2]).map(move |visible| { + counter.fetch_add(1, Ordering::SeqCst); + Ok(Bytes::from_owner(BackingAllocation { + data: vec![42; CHUNK_SIZE * 64], + visible, + live: counter.clone(), + })) + }); + let mut reader = HttpResponseReader::new(Box::pin(body)); + reader.read_more().await; + assert_eq!(live.load(Ordering::SeqCst), 0); + reader.read_more().await; + reader.read_more().await; + assert_eq!(live.load(Ordering::SeqCst), 0); + reader.read_more().await; + assert_eq!(live.load(Ordering::SeqCst), 1); + assert_eq!(reader.pending.len(), CHUNK_SIZE + 2); + let mut output = Vec::new(); + assert!(reader.next_chunk(&mut output).await.unwrap()); + reader.read_more().await; + assert_eq!(live.load(Ordering::SeqCst), 1); + assert!(reader.next_chunk(&mut output).await.unwrap()); + reader.read_more().await; + assert_eq!(live.load(Ordering::SeqCst), 0); +} + +#[tokio::test] +async fn flushes_last_partial_before_upstream_error_or_panic() { + for panic in [false, true] { + let body = + stream::iter([Ok(Bytes::from_static(b"partial"))]).chain(stream::once(async move { + assert!(!panic, "failing user stream"); + Err(CopilotRequestError::message("upstream failed")) + })); + let mut reader = HttpResponseReader::new(Box::pin(body)); + reader.read_more().await; + reader.read_more().await; + let mut output = Vec::new(); + assert!(reader.next_chunk(&mut output).await.unwrap()); + assert_eq!(output, b"partial"); + let error = reader.next_chunk(&mut output).await.unwrap_err(); + assert_eq!( + error.to_string(), + if panic { + "HTTP response body stream panicked" + } else { + "upstream failed" + } + ); + } +} + +#[tokio::test] +async fn dropping_reader_drops_pending_source() { + let (tx, rx) = tokio::sync::mpsc::channel(1); + let mut reader = + HttpResponseReader::new(Box::pin(tokio_stream::wrappers::ReceiverStream::new(rx))); + assert!(reader.read_more().now_or_never().is_none()); + drop(reader); + assert!(tx.is_closed()); +} + +#[tokio::test] +async fn always_ready_empty_frames_yield_to_cancellation() { + let mut reader = HttpResponseReader::new(Box::pin(stream::repeat_with(|| Ok(Bytes::new())))); + let mut output = Vec::new(); + assert!( + tokio::time::timeout(Duration::from_millis(10), reader.next_chunk(&mut output)) + .await + .is_err() + ); +} diff --git a/rust/src/embeddedcli.rs b/rust/src/embeddedcli.rs index 67437be998..3baebd13f1 100644 --- a/rust/src/embeddedcli.rs +++ b/rust/src/embeddedcli.rs @@ -1126,818 +1126,4 @@ impl std::error::Error for EmbeddedCliError { } #[cfg(test)] -mod tests { - use super::*; - - #[cfg(all(has_bundled_cli, feature = "in-process"))] - #[test] - fn embedded_runtime_archive_contains_runtime_assets_and_excludes_cli() { - let gz = flate2::read::GzDecoder::new(build_time::RUNTIME_ARCHIVE); - let mut archive = tar::Archive::new(gz); - let mut names: Vec = archive - .entries() - .expect("archive entries") - .map(|entry| { - entry - .expect("archive entry") - .path() - .expect("archive path") - .to_string_lossy() - .into_owned() - }) - .collect(); - names.sort(); - - assert!(names.contains(&RUNTIME_LIBRARY_NAME.to_string())); - assert!(names.contains(&RUNTIME_BINARY_NAME.to_string())); - assert!(names.contains(&RUNTIME_NODE_NAME.to_string())); - assert!(names.iter().any(|name| name.starts_with("ripgrep/"))); - assert!(names.iter().any(|name| name.starts_with("definitions/"))); - assert!(!names.contains(&CLI_BINARY_NAME.to_string())); - assert!(!names.contains(&"app.js".to_string())); - } - - /// Bytes whose header looks like a valid executable image on the host - /// platform, so `looks_like_valid_image` accepts them. `extra` padding - /// bytes follow the magic so size checks have something to disagree about. - fn fake_image(extra: usize) -> Vec { - let mut bytes = Vec::new(); - #[cfg(windows)] - bytes.extend_from_slice(b"MZ\x90\x00"); - #[cfg(target_os = "macos")] - bytes.extend_from_slice(&[0xfe, 0xed, 0xfa, 0xcf]); - #[cfg(all(not(windows), not(target_os = "macos")))] - bytes.extend_from_slice(b"\x7fELF"); - bytes.extend(std::iter::repeat_n(0xAB, extra)); - bytes - } - - #[test] - fn publish_verified_writes_and_records_marker() { - let dir = tempfile::tempdir().expect("tempdir"); - let final_path = dir.path().join("copilot-bin"); - let marker = marker_path(dir.path()); - let bytes = fake_image(2048); - - publish_verified(dir.path(), &final_path, &marker, &bytes).expect("publish"); - - assert!(final_path.is_file(), "binary should be published"); - assert_eq!(fs::read(&final_path).expect("read"), bytes); - assert_eq!(read_marker_len(&marker), Some(bytes.len() as u64)); - assert!(existing_install_is_valid( - &final_path, - &marker, - bytes.len() as u64 - )); - - // No leftover temp files in the install dir. - let leftovers: Vec<_> = fs::read_dir(dir.path()) - .expect("read_dir") - .filter_map(|e| e.ok()) - .filter(|e| e.file_name().to_string_lossy().contains(".tmp.")) - .collect(); - assert!(leftovers.is_empty(), "temp files should be cleaned up"); - } - - #[test] - fn publish_overwrites_an_existing_binary() { - let dir = tempfile::tempdir().expect("tempdir"); - let final_path = dir.path().join("copilot-bin"); - let marker = marker_path(dir.path()); - - // Pre-existing (stale) binary at the destination. - fs::write(&final_path, b"old contents").expect("seed"); - - let bytes = fake_image(512); - publish_verified(dir.path(), &final_path, &marker, &bytes).expect("publish"); - - assert_eq!(fs::read(&final_path).expect("read"), bytes); - } - - #[test] - fn corrupt_or_unmarked_install_is_rejected() { - let dir = tempfile::tempdir().expect("tempdir"); - let final_path = dir.path().join("copilot-bin"); - let marker = marker_path(dir.path()); - let bytes = fake_image(4096); - - // Missing binary entirely. - assert!(!existing_install_is_valid(&final_path, &marker, 1)); - - // Valid binary but no marker (e.g. installed by an older SDK). - fs::write(&final_path, &bytes).expect("write binary"); - assert!( - !existing_install_is_valid(&final_path, &marker, bytes.len() as u64), - "an install without a marker must not be trusted" - ); - - // Marker present but the binary was later truncated (partial write / - // antivirus). Marker still records the original full size. - write_marker(&marker, bytes.len() as u64).expect("marker"); - assert!(existing_install_is_valid( - &final_path, - &marker, - bytes.len() as u64 - )); - assert!( - !existing_install_is_valid(&final_path, &marker, bytes.len() as u64 + 1), - "a marker from the wrapper-as-CLI regression must not validate the full CLI" - ); - fs::write(&final_path, &bytes[..bytes.len() / 2]).expect("truncate"); - assert!( - !existing_install_is_valid(&final_path, &marker, bytes.len() as u64), - "a truncated binary must be detected via the size marker" - ); - - // Zero-length binary (quarantined to empty). - fs::write(&final_path, b"").expect("empty"); - assert!(!existing_install_is_valid( - &final_path, - &marker, - bytes.len() as u64 - )); - } - - #[test] - fn invalid_image_header_is_rejected() { - let dir = tempfile::tempdir().expect("tempdir"); - let final_path = dir.path().join("copilot-bin"); - let marker = marker_path(dir.path()); - - // Right size, has a marker, but the bytes are not a valid image. - let garbage = vec![0u8; 4096]; - fs::write(&final_path, &garbage).expect("write garbage"); - write_marker(&marker, garbage.len() as u64).expect("marker"); - - assert!( - !existing_install_is_valid(&final_path, &marker, garbage.len() as u64), - "a non-executable image must be rejected even with a matching marker" - ); - } - - #[test] - fn verification_rejects_size_and_content_mismatch() { - let dir = tempfile::tempdir().expect("tempdir"); - let path = dir.path().join("staged"); - let expected = fake_image(1024); - - // Exact match passes. - fs::write(&path, &expected).expect("write"); - verify_on_disk_matches(&path, &expected).expect("exact match should verify"); - - // Truncated -> size mismatch. - fs::write(&path, &expected[..100]).expect("truncate"); - assert!(verify_on_disk_matches(&path, &expected).is_err()); - - // Same length, different bytes -> content mismatch. - let mut tampered = expected.clone(); - *tampered.last_mut().expect("non-empty") ^= 0xFF; - fs::write(&path, &tampered).expect("tamper"); - assert!(verify_on_disk_matches(&path, &expected).is_err()); - - // Missing file -> I/O error. - fs::remove_file(&path).expect("remove"); - assert!(verify_on_disk_matches(&path, &expected).is_err()); - } - - #[test] - fn temp_files_are_unique_and_synced() { - let dir = tempfile::tempdir().expect("tempdir"); - let data = fake_image(256); - - let a = write_temp_file(dir.path(), &data).expect("temp a"); - let b = write_temp_file(dir.path(), &data).expect("temp b"); - - assert_ne!(a, b, "temp file names must be unique"); - assert_eq!(fs::read(&a).expect("read a"), data); - assert_eq!(fs::read(&b).expect("read b"), data); - - #[cfg(unix)] - { - use std::os::unix::fs::PermissionsExt; - let mode = fs::metadata(&a).expect("meta").permissions().mode(); - assert_eq!(mode & 0o777, 0o755, "temp binary should be executable"); - } - } - - #[cfg(has_bundled_cli)] - #[test] - fn runtime_install_replaces_stale_pair() { - let dir = tempfile::tempdir().expect("tempdir"); - fs::write(dir.path().join(RUNTIME_NODE_NAME), b"stale runtime").expect("seed runtime"); - fs::write(dir.path().join(RUNTIME_BINARY_NAME), b"stale wrapper").expect("seed wrapper"); - - install_runtime(dir.path(), build_time::RUNTIME_ARCHIVE).expect("install runtime"); - - assert_eq!( - fs::read(dir.path().join(RUNTIME_NODE_NAME)).expect("read runtime"), - extract_binary(build_time::RUNTIME_ARCHIVE, RUNTIME_NODE_NAME) - .expect("extract runtime") - ); - assert_eq!( - fs::read(dir.path().join(RUNTIME_BINARY_NAME)).expect("read wrapper"), - extract_binary(build_time::RUNTIME_ARCHIVE, RUNTIME_BINARY_NAME) - .expect("extract wrapper") - ); - } - - #[cfg(has_bundled_cli)] - #[test] - fn custom_runtime_install_dir_isolated_by_version() { - let dir = tempfile::tempdir().expect("tempdir"); - - assert_eq!( - runtime_install_dir(dir.path(), "1.0.0").expect("claim directory"), - dir.path() - ); - assert_eq!( - runtime_install_dir(dir.path(), "1.0.0").expect("reuse directory"), - dir.path() - ); - assert_eq!( - runtime_install_dir(dir.path(), "2.0.0").expect("isolate directory"), - dir.path().join("2.0.0") - ); - } - - #[cfg(has_bundled_cli)] - fn runtime_fixture(extra: &[(&str, &[u8], u32)]) -> Vec { - let encoder = flate2::write::GzEncoder::new(Vec::new(), flate2::Compression::fast()); - let mut archive = tar::Builder::new(encoder); - let mut entries = vec![ - (RUNTIME_BINARY_NAME, b"wrapper".as_slice(), 0o755), - (RUNTIME_NODE_NAME, b"runtime".as_slice(), 0o755), - ]; - #[cfg(feature = "in-process")] - entries.push((RUNTIME_LIBRARY_NAME, b"library".as_slice(), 0o644)); - entries.extend_from_slice(extra); - for (name, bytes, mode) in entries { - let mut header = tar::Header::new_gnu(); - // Raw names also let the installer see traversal fixtures which - // Builder::append_data would reject before reaching product code. - header.as_mut_bytes()[..name.len()].copy_from_slice(name.as_bytes()); - header.set_size(bytes.len() as u64); - header.set_mode(mode); - header.set_cksum(); - archive.append(&header, bytes).expect("append fixture"); - } - archive.into_inner().unwrap().finish().unwrap() - } - - #[cfg(has_bundled_cli)] - #[test] - fn warm_runtime_rejects_same_size_corruption_despite_unchanged_metadata() { - let dir = tempfile::tempdir().unwrap(); - let fixture = runtime_fixture(&[]); - install_runtime(dir.path(), &fixture).unwrap(); - let runtime = dir.path().join(RUNTIME_NODE_NAME); - let original = fs::metadata(&runtime).unwrap(); - fs::write(&runtime, b"corrupt").unwrap(); - fs::File::options() - .write(true) - .open(&runtime) - .unwrap() - .set_times(fs::FileTimes::new().set_modified(original.modified().unwrap())) - .unwrap(); - assert!(install_runtime(dir.path(), b"invalid archive").is_err()); - assert_eq!(fs::read(&runtime).unwrap(), b"corrupt"); - install_runtime(dir.path(), &fixture).unwrap(); - assert_eq!(fs::read(&runtime).unwrap(), b"runtime"); - } - - #[cfg(has_bundled_cli)] - fn assert_no_runtime_temps(dir: &Path) { - for entry in fs::read_dir(dir).unwrap() { - let entry = entry.unwrap(); - assert!( - !entry - .file_name() - .to_string_lossy() - .starts_with(".copilot-cli.tmp.") - ); - if entry.file_type().unwrap().is_dir() { - assert_no_runtime_temps(&entry.path()); - } - } - } - - #[cfg(has_bundled_cli)] - #[test] - fn runtime_cold_install_and_warm_reuse_preserve_bytes_and_modes() { - let dir = tempfile::tempdir().unwrap(); - let data = vec![0xAB; 3 * 64 * 1024 + 17]; - let archive = runtime_fixture(&[("nested/asset", &data, 0o640)]); - let wrapper = install_runtime(dir.path(), &archive).unwrap(); - assert_eq!(wrapper, dir.path().join(RUNTIME_BINARY_NAME)); - let asset = dir.path().join("nested/asset"); - assert_eq!(fs::read(&asset).unwrap(), data); - let modified = std::time::UNIX_EPOCH + std::time::Duration::from_secs(1234567890); - fs::File::options() - .write(true) - .open(&asset) - .unwrap() - .set_times(fs::FileTimes::new().set_modified(modified)) - .unwrap(); - - install_runtime(dir.path(), &archive).unwrap(); - - assert_eq!(fs::metadata(&asset).unwrap().modified().unwrap(), modified); - assert_eq!(fs::read(&asset).unwrap(), data); - #[cfg(unix)] - { - use std::os::unix::fs::PermissionsExt; - assert_eq!( - fs::metadata(&asset).unwrap().permissions().mode() & 0o777, - 0o640 - ); - assert_eq!( - fs::metadata(wrapper).unwrap().permissions().mode() & 0o777, - 0o755 - ); - } - assert_no_runtime_temps(dir.path()); - } - - #[cfg(has_bundled_cli)] - #[test] - fn runtime_repairs_same_size_corruption_truncation_and_extra_bytes() { - let dir = tempfile::tempdir().unwrap(); - let archive = runtime_fixture(&[]); - let runtime = dir.path().join(RUNTIME_NODE_NAME); - install_runtime(dir.path(), &archive).unwrap(); - - for corrupt in [b"runtimX".as_slice(), b"run", b"", b"runtime plus garbage"] { - fs::write(&runtime, corrupt).unwrap(); - install_runtime(dir.path(), &archive).unwrap(); - assert_eq!(fs::read(&runtime).unwrap(), b"runtime"); - assert_no_runtime_temps(dir.path()); - } - } - - #[cfg(has_bundled_cli)] - #[test] - fn runtime_repairs_corruption_across_comparison_chunks() { - let dir = tempfile::tempdir().unwrap(); - let bytes = vec![0xAB; 3 * 64 * 1024 + 17]; - let archive = runtime_fixture(&[("nested/asset", &bytes, 0o644)]); - install_runtime(dir.path(), &archive).unwrap(); - for offset in [0, bytes.len() / 2, bytes.len() - 1] { - let mut corrupt = bytes.clone(); - corrupt[offset] ^= 1; - fs::write(dir.path().join("nested/asset"), &corrupt).unwrap(); - install_runtime(dir.path(), &archive).unwrap(); - assert_eq!(fs::read(dir.path().join("nested/asset")).unwrap(), bytes); - assert_no_runtime_temps(dir.path()); - } - } - - #[cfg(all(has_bundled_cli, unix))] - #[test] - fn runtime_reuse_preserves_executable_caller_selected_permissions() { - use std::os::unix::fs::PermissionsExt; - - let dir = tempfile::tempdir().unwrap(); - let fixture = runtime_fixture(&[]); - let wrapper = install_runtime(dir.path(), &fixture).unwrap(); - for mode in [0o745, 0o754, 0o700, 0o500] { - fs::set_permissions(&wrapper, fs::Permissions::from_mode(mode)).unwrap(); - install_runtime(dir.path(), &fixture).unwrap(); - assert_eq!( - fs::metadata(&wrapper).unwrap().permissions().mode() & 0o777, - mode - ); - } - assert_no_runtime_temps(dir.path()); - } - - #[cfg(all(has_bundled_cli, unix))] - #[test] - fn runtime_repairs_nonexecutable_wrapper() { - use std::os::unix::fs::PermissionsExt; - - let dir = tempfile::tempdir().unwrap(); - let archive = runtime_fixture(&[]); - let wrapper = install_runtime(dir.path(), &archive).unwrap(); - for mode in [0o400, 0o600] { - fs::set_permissions(&wrapper, fs::Permissions::from_mode(mode)).unwrap(); - install_runtime(dir.path(), &archive).unwrap(); - assert_eq!( - fs::metadata(&wrapper).unwrap().permissions().mode() & 0o777, - 0o755 - ); - assert_eq!(fs::read(&wrapper).unwrap(), b"wrapper"); - assert_no_runtime_temps(dir.path()); - } - } - - #[cfg(all(has_bundled_cli, unix))] - #[test] - fn runtime_rejects_nonexecutable_wrapper_in_readonly_cache() { - use std::os::unix::fs::PermissionsExt; - - let dir = tempfile::tempdir().unwrap(); - let archive = runtime_fixture(&[]); - let wrapper = install_runtime(dir.path(), &archive).unwrap(); - fs::set_permissions(&wrapper, fs::Permissions::from_mode(0o400)).unwrap(); - fs::set_permissions(dir.path(), fs::Permissions::from_mode(0o555)).unwrap(); - let write_denied = fs::File::create(dir.path().join("write-probe")).is_err(); - let result = install_runtime(dir.path(), &archive); - fs::set_permissions(dir.path(), fs::Permissions::from_mode(0o755)).unwrap(); - if write_denied { - assert!( - result.is_err(), - "returned a nonexecutable wrapper: {result:?}" - ); - assert_eq!( - fs::metadata(&wrapper).unwrap().permissions().mode() & 0o777, - 0o400 - ); - } else { - eprintln!("read-only permission enforcement unavailable (e.g. privileged user)"); - fs::remove_file(dir.path().join("write-probe")).unwrap(); - result.unwrap(); - assert_eq!( - fs::metadata(&wrapper).unwrap().permissions().mode() & 0o777, - 0o755 - ); - } - assert_no_runtime_temps(dir.path()); - } - - #[cfg(has_bundled_cli)] - #[test] - fn runtime_archive_errors_do_not_publish_and_clean_up_staging() { - let dir = tempfile::tempdir().unwrap(); - let valid = runtime_fixture(&[("nested/asset", b"asset", 0o644)]); - let mut invalid_crc = valid.clone(); - let crc = invalid_crc.len() - 8; - invalid_crc[crc] ^= 0xFF; - let mut invalid_length = valid.clone(); - let length = invalid_length.len() - 4; - invalid_length[length] ^= 0xFF; - let mut truncated_trailer = valid.clone(); - truncated_trailer.truncate(valid.len() - 1); - let mut truncated_body = valid.clone(); - truncated_body.truncate(valid.len() / 2); - for archive in [ - invalid_crc, - invalid_length, - truncated_trailer, - truncated_body, - ] { - let runtime = dir.path().join(RUNTIME_NODE_NAME); - fs::write(&runtime, b"previous complete runtime").unwrap(); - assert!(install_runtime(dir.path(), &archive).is_err()); - assert_eq!(fs::read(runtime).unwrap(), b"previous complete runtime"); - assert!(!dir.path().join(RUNTIME_BINARY_NAME).exists()); - assert!(!dir.path().join("nested/asset").exists()); - assert_no_runtime_temps(dir.path()); - } - } - - #[cfg(has_bundled_cli)] - #[test] - fn runtime_short_entry_is_rejected_without_publishing() { - let dir = tempfile::tempdir().unwrap(); - let mut header = tar::Header::new_gnu(); - header.set_path(RUNTIME_NODE_NAME).unwrap(); - header.set_size(128 * 1024); - header.set_mode(0o755); - header.set_cksum(); - let mut encoder = flate2::write::GzEncoder::new(Vec::new(), flate2::Compression::fast()); - encoder.write_all(header.as_bytes()).unwrap(); - encoder.write_all(b"short").unwrap(); - let archive = encoder.finish().unwrap(); - - assert!(install_runtime(dir.path(), &archive).is_err()); - assert!(!dir.path().join(RUNTIME_NODE_NAME).exists()); - assert_no_runtime_temps(dir.path()); - } - - #[cfg(has_bundled_cli)] - #[test] - fn runtime_missing_required_entry_is_rejected_before_publish() { - let dir = tempfile::tempdir().unwrap(); - let encoder = flate2::write::GzEncoder::new(Vec::new(), flate2::Compression::fast()); - let mut archive = tar::Builder::new(encoder); - let mut header = tar::Header::new_gnu(); - header.set_size(7); - header.set_mode(0o755); - header.set_cksum(); - archive - .append_data(&mut header, RUNTIME_BINARY_NAME, b"wrapper".as_slice()) - .unwrap(); - let archive = archive.into_inner().unwrap().finish().unwrap(); - - assert!(install_runtime(dir.path(), &archive).is_err()); - assert!(!dir.path().join(RUNTIME_BINARY_NAME).exists()); - assert_no_runtime_temps(dir.path()); - } - - #[cfg(has_bundled_cli)] - #[test] - fn runtime_rejects_traversal_and_cleans_preceding_entries() { - let dir = tempfile::tempdir().unwrap(); - let install_dir = dir.path().join("install"); - let archive = runtime_fixture(&[("../escaped", b"bad", 0o644)]); - assert!(install_runtime(&install_dir, &archive).is_err()); - assert!(!dir.path().join("escaped").exists()); - assert!(!install_dir.join(RUNTIME_BINARY_NAME).exists()); - assert_no_runtime_temps(&install_dir); - } - - #[cfg(all(has_bundled_cli, unix))] - #[test] - fn runtime_rejects_symlink_parents_and_targets() { - use std::os::unix::fs::symlink; - - let dir = tempfile::tempdir().unwrap(); - let outside = tempfile::tempdir().unwrap(); - fs::write(outside.path().join("asset"), b"outside").unwrap(); - symlink(outside.path(), dir.path().join("nested")).unwrap(); - let archive = runtime_fixture(&[("nested/asset", b"new", 0o644)]); - - assert!(install_runtime(dir.path(), &archive).is_err()); - assert_eq!(fs::read(outside.path().join("asset")).unwrap(), b"outside"); - assert_no_runtime_temps(dir.path()); - fs::remove_file(dir.path().join("nested")).unwrap(); - symlink( - outside.path().join("asset"), - dir.path().join(RUNTIME_NODE_NAME), - ) - .unwrap(); - assert!(install_runtime(dir.path(), &archive).is_err()); - assert_eq!(fs::read(outside.path().join("asset")).unwrap(), b"outside"); - assert_no_runtime_temps(dir.path()); - } - - #[cfg(has_bundled_cli)] - #[test] - fn concurrent_runtime_installers_publish_complete_files() { - let dir = tempfile::tempdir().unwrap(); - let data = vec![0xAB; 256 * 1024 + 1]; - let archive = runtime_fixture(&[("nested/asset", &data, 0o644)]); - let barrier = std::sync::Barrier::new(6); - std::thread::scope(|scope| { - for _ in 0..6 { - scope.spawn(|| { - barrier.wait(); - install_runtime(dir.path(), &archive).unwrap(); - }); - } - }); - assert_eq!(fs::read(dir.path().join("nested/asset")).unwrap(), data); - assert_eq!( - fs::read(dir.path().join(RUNTIME_NODE_NAME)).unwrap(), - b"runtime" - ); - assert_no_runtime_temps(dir.path()); - } - - #[cfg(has_bundled_cli)] - #[test] - fn runtime_duplicate_destinations_fail_on_cold_and_warm_installs() { - for name in ["asset", "./asset"] { - let dir = tempfile::tempdir().unwrap(); - let archive = runtime_fixture(&[("asset", b"first", 0o644), (name, b"last", 0o644)]); - assert!(install_runtime(dir.path(), &archive).is_err()); - assert!(!dir.path().join("asset").exists()); - assert_no_runtime_temps(dir.path()); - - fs::write(dir.path().join("asset"), b"last").unwrap(); - assert!(install_runtime(dir.path(), &archive).is_err()); - assert_eq!(fs::read(dir.path().join("asset")).unwrap(), b"last"); - assert_no_runtime_temps(dir.path()); - } - let dir = tempfile::tempdir().unwrap(); - let archive = runtime_fixture(&[(RUNTIME_NODE_NAME, b"", 0o755)]); - assert!(install_runtime(dir.path(), &archive).is_err()); - assert!(!dir.path().join(RUNTIME_NODE_NAME).exists()); - assert_no_runtime_temps(dir.path()); - install_runtime(dir.path(), &runtime_fixture(&[])).unwrap(); - assert!(install_runtime(dir.path(), &archive).is_err()); - assert_eq!( - fs::read(dir.path().join(RUNTIME_NODE_NAME)).unwrap(), - b"runtime" - ); - assert_no_runtime_temps(dir.path()); - } - - #[cfg(has_bundled_cli)] - #[test] - fn runtime_case_aliases_cannot_overwrite_required_artifacts() { - let names = [ - RUNTIME_NODE_NAME, - RUNTIME_BINARY_NAME, - #[cfg(feature = "in-process")] - RUNTIME_LIBRARY_NAME, - ]; - for name in names { - let alias = name.to_ascii_uppercase(); - let archive = runtime_fixture(&[(&alias, b"", 0o755)]); - let dir = tempfile::tempdir().unwrap(); - let result = install_runtime(dir.path(), &archive); - assert!(result.is_err(), "accepted alias {alias}: {result:?}"); - assert!(!dir.path().join(name).exists()); - assert_no_runtime_temps(dir.path()); - - install_runtime(dir.path(), &runtime_fixture(&[])).unwrap(); - let original = fs::read(dir.path().join(name)).unwrap(); - assert!(install_runtime(dir.path(), &archive).is_err()); - assert_eq!(fs::read(dir.path().join(name)).unwrap(), original); - fs::write(dir.path().join(name), b"corrupt").unwrap(); - assert!(install_runtime(dir.path(), &archive).is_err()); - assert_eq!(fs::read(dir.path().join(name)).unwrap(), b"corrupt"); - assert_no_runtime_temps(dir.path()); - } - } - - #[cfg(has_bundled_cli)] - #[test] - fn runtime_mixed_separator_and_case_aliases_are_rejected() { - let dir = tempfile::tempdir().unwrap(); - let archive = runtime_fixture(&[ - ("nested/asset", b"first", 0o644), - (r".\NESTED\ASSET", b"last", 0o644), - ]); - assert!(install_runtime(dir.path(), &archive).is_err()); - assert!(!dir.path().join("nested/asset").exists()); - assert_no_runtime_temps(dir.path()); - - install_runtime( - dir.path(), - &runtime_fixture(&[("nested/asset", b"last", 0o644)]), - ) - .unwrap(); - assert!(install_runtime(dir.path(), &archive).is_err()); - assert_eq!(fs::read(dir.path().join("nested/asset")).unwrap(), b"last"); - assert_no_runtime_temps(dir.path()); - } - - #[cfg(has_bundled_cli)] - #[test] - fn runtime_nonportable_alias_paths_are_rejected_before_publish() { - for name in [ - "runtime.node.", - "runtime.node ", - "runtime.node:stream", - "RUNTIM~1.NOD", - "NUL", - "con.txt", - "aux .txt", - "COM1", - "LPT9.txt", - "n\u{00e9}sted/asset", - r"..\escaped", - r"C:\escaped", - r"\\server\share\asset", - ] { - let dir = tempfile::tempdir().unwrap(); - let archive = runtime_fixture(&[(name, b"bad", 0o644)]); - assert!( - install_runtime(dir.path(), &archive).is_err(), - "accepted {name}" - ); - assert!(!dir.path().join(RUNTIME_NODE_NAME).exists()); - assert_no_runtime_temps(dir.path()); - } - } - - #[cfg(all(has_bundled_cli, feature = "in-process"))] - #[test] - fn runtime_library_aliases_are_rejected_before_repair() { - let alias = format!("./{RUNTIME_LIBRARY_NAME}"); - for duplicate in [b"another".as_slice(), b""] { - let dir = tempfile::tempdir().unwrap(); - let archive = runtime_fixture(&[(&alias, duplicate, 0o644)]); - assert!(install_runtime(dir.path(), &archive).is_err()); - assert!(!dir.path().join(RUNTIME_LIBRARY_NAME).exists()); - assert_no_runtime_temps(dir.path()); - - install_runtime(dir.path(), &runtime_fixture(&[])).unwrap(); - let library = dir.path().join(RUNTIME_LIBRARY_NAME); - assert!(install_runtime(dir.path(), &archive).is_err()); - assert_eq!(fs::read(&library).unwrap(), b"library"); - fs::write(&library, b"corrupt").unwrap(); - assert!(install_runtime(dir.path(), &archive).is_err()); - assert_eq!(fs::read(&library).unwrap(), b"corrupt"); - assert_no_runtime_temps(dir.path()); - } - } - - #[cfg(all(has_bundled_cli, unix))] - #[test] - fn warm_runtime_install_needs_no_writable_cache() { - assert_read_only_runtime_cache(0o555, false); - } - - #[cfg(all(has_bundled_cli, unix))] - #[test] - fn warm_runtime_reuses_owner_only_immutable_cache() { - assert_read_only_runtime_cache(0o500, false); - } - - #[cfg(all(has_bundled_cli, unix))] - #[test] - fn warm_runtime_reuses_nonexecutable_native_library() { - assert_read_only_runtime_cache(0o555, true); - assert_read_only_runtime_cache(0o500, true); - } - - #[cfg(all(has_bundled_cli, unix))] - fn assert_read_only_runtime_cache(permission_mask: u32, readonly_native_library: bool) { - use std::os::unix::fs::PermissionsExt; - - let dir = tempfile::tempdir().unwrap(); - let archive = runtime_fixture(&[("nested/asset", b"asset", 0o644)]); - install_runtime(dir.path(), &archive).unwrap(); - let files = [ - RUNTIME_BINARY_NAME, - RUNTIME_NODE_NAME, - "nested/asset", - #[cfg(feature = "in-process")] - RUNTIME_LIBRARY_NAME, - ]; - let mut read_only_files = Vec::new(); - for name in files { - let path = dir.path().join(name); - let mut mode = fs::metadata(&path).unwrap().permissions().mode() & permission_mask; - if readonly_native_library && name != RUNTIME_BINARY_NAME { - mode &= !0o111; - } - fs::set_permissions(&path, fs::Permissions::from_mode(mode)).unwrap(); - read_only_files.push((path, mode)); - } - fs::set_permissions( - dir.path().join("nested"), - fs::Permissions::from_mode(permission_mask), - ) - .unwrap(); - fs::set_permissions(dir.path(), fs::Permissions::from_mode(permission_mask)).unwrap(); - let write_denied = fs::File::create(dir.path().join("write-probe")).is_err(); - let result = install_runtime(dir.path(), &archive); - fs::set_permissions(dir.path(), fs::Permissions::from_mode(0o755)).unwrap(); - fs::set_permissions(dir.path().join("nested"), fs::Permissions::from_mode(0o755)).unwrap(); - result.unwrap(); - if !write_denied { - eprintln!("read-only permission enforcement unavailable (e.g. privileged user)"); - fs::remove_file(dir.path().join("write-probe")).unwrap(); - } - for (path, mode) in read_only_files { - assert_eq!( - fs::metadata(path).unwrap().permissions().mode() & 0o777, - mode - ); - } - assert_eq!(fs::read(dir.path().join("nested/asset")).unwrap(), b"asset"); - assert_no_runtime_temps(dir.path()); - } - - #[cfg(all(has_bundled_cli, unix))] - #[test] - fn runtime_repairs_unreadable_but_replaceable_file() { - use std::os::unix::fs::PermissionsExt; - - let dir = tempfile::tempdir().unwrap(); - let archive = runtime_fixture(&[("asset", b"trusted", 0o000)]); - let asset = dir.path().join("asset"); - fs::write(&asset, b"corrupt").unwrap(); - fs::set_permissions(&asset, fs::Permissions::from_mode(0o000)).unwrap(); - if fs::File::open(&asset).is_ok() { - eprintln!("unreadable permission enforcement unavailable (e.g. privileged user)"); - } - let result = install_runtime(dir.path(), &archive); - let mode = fs::metadata(&asset).unwrap().permissions().mode() & 0o777; - fs::set_permissions(&asset, fs::Permissions::from_mode(0o600)).unwrap(); - result.unwrap(); - assert_eq!(mode, 0o000); - assert_eq!(fs::read(asset).unwrap(), b"trusted"); - assert_no_runtime_temps(dir.path()); - } - - #[cfg(has_bundled_cli)] - #[test] - fn runtime_replacement_preserves_open_reader_contents() { - let dir = tempfile::tempdir().unwrap(); - let asset = dir.path().join("asset"); - let original = runtime_fixture(&[("asset", b"old complete file", 0o644)]); - install_runtime(dir.path(), &original).unwrap(); - let mut reader = fs::File::open(&asset).unwrap(); - let replacement = runtime_fixture(&[("asset", b"new complete file", 0o644)]); - install_runtime(dir.path(), &replacement).unwrap(); - let mut bytes = Vec::new(); - reader.read_to_end(&mut bytes).unwrap(); - assert_eq!(bytes, b"old complete file"); - assert_eq!(fs::read(&asset).unwrap(), b"new complete file"); - assert_no_runtime_temps(dir.path()); - } - - #[test] - fn failed_publish_does_not_remove_previous_file() { - let dir = tempfile::tempdir().unwrap(); - let target = dir.path().join("installed"); - fs::write(&target, b"previous complete file").unwrap(); - assert!(publish(&dir.path().join("missing-temporary"), &target).is_err()); - assert_eq!(fs::read(target).unwrap(), b"previous complete file"); - } -} +mod tests; diff --git a/rust/src/embeddedcli/tests.rs b/rust/src/embeddedcli/tests.rs new file mode 100644 index 0000000000..ab7d5281ac --- /dev/null +++ b/rust/src/embeddedcli/tests.rs @@ -0,0 +1,818 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +#![cfg(test)] + +use super::*; + +#[cfg(all(has_bundled_cli, feature = "in-process"))] +#[test] +fn embedded_runtime_archive_contains_runtime_assets_and_excludes_cli() { + let gz = flate2::read::GzDecoder::new(build_time::RUNTIME_ARCHIVE); + let mut archive = tar::Archive::new(gz); + let mut names: Vec = archive + .entries() + .expect("archive entries") + .map(|entry| { + entry + .expect("archive entry") + .path() + .expect("archive path") + .to_string_lossy() + .into_owned() + }) + .collect(); + names.sort(); + + assert!(names.contains(&RUNTIME_LIBRARY_NAME.to_string())); + assert!(names.contains(&RUNTIME_BINARY_NAME.to_string())); + assert!(names.contains(&RUNTIME_NODE_NAME.to_string())); + assert!(names.iter().any(|name| name.starts_with("ripgrep/"))); + assert!(names.iter().any(|name| name.starts_with("definitions/"))); + assert!(!names.contains(&CLI_BINARY_NAME.to_string())); + assert!(!names.contains(&"app.js".to_string())); + assert!(!names.contains(&"cli-main.js".to_string())); +} + +/// Bytes whose header looks like a valid executable image on the host +/// platform, so `looks_like_valid_image` accepts them. `extra` padding +/// bytes follow the magic so size checks have something to disagree about. +fn fake_image(extra: usize) -> Vec { + let mut bytes = Vec::new(); + #[cfg(windows)] + bytes.extend_from_slice(b"MZ\x90\x00"); + #[cfg(target_os = "macos")] + bytes.extend_from_slice(&[0xfe, 0xed, 0xfa, 0xcf]); + #[cfg(all(not(windows), not(target_os = "macos")))] + bytes.extend_from_slice(b"\x7fELF"); + bytes.extend(std::iter::repeat_n(0xAB, extra)); + bytes +} + +#[test] +fn publish_verified_writes_and_records_marker() { + let dir = tempfile::tempdir().expect("tempdir"); + let final_path = dir.path().join("copilot-bin"); + let marker = marker_path(dir.path()); + let bytes = fake_image(2048); + + publish_verified(dir.path(), &final_path, &marker, &bytes).expect("publish"); + + assert!(final_path.is_file(), "binary should be published"); + assert_eq!(fs::read(&final_path).expect("read"), bytes); + assert_eq!(read_marker_len(&marker), Some(bytes.len() as u64)); + assert!(existing_install_is_valid( + &final_path, + &marker, + bytes.len() as u64 + )); + + // No leftover temp files in the install dir. + let leftovers: Vec<_> = fs::read_dir(dir.path()) + .expect("read_dir") + .filter_map(|e| e.ok()) + .filter(|e| e.file_name().to_string_lossy().contains(".tmp.")) + .collect(); + assert!(leftovers.is_empty(), "temp files should be cleaned up"); +} + +#[test] +fn publish_overwrites_an_existing_binary() { + let dir = tempfile::tempdir().expect("tempdir"); + let final_path = dir.path().join("copilot-bin"); + let marker = marker_path(dir.path()); + + // Pre-existing (stale) binary at the destination. + fs::write(&final_path, b"old contents").expect("seed"); + + let bytes = fake_image(512); + publish_verified(dir.path(), &final_path, &marker, &bytes).expect("publish"); + + assert_eq!(fs::read(&final_path).expect("read"), bytes); +} + +#[test] +fn corrupt_or_unmarked_install_is_rejected() { + let dir = tempfile::tempdir().expect("tempdir"); + let final_path = dir.path().join("copilot-bin"); + let marker = marker_path(dir.path()); + let bytes = fake_image(4096); + + // Missing binary entirely. + assert!(!existing_install_is_valid(&final_path, &marker, 1)); + + // Valid binary but no marker (e.g. installed by an older SDK). + fs::write(&final_path, &bytes).expect("write binary"); + assert!( + !existing_install_is_valid(&final_path, &marker, bytes.len() as u64), + "an install without a marker must not be trusted" + ); + + // Marker present but the binary was later truncated (partial write / + // antivirus). Marker still records the original full size. + write_marker(&marker, bytes.len() as u64).expect("marker"); + assert!(existing_install_is_valid( + &final_path, + &marker, + bytes.len() as u64 + )); + assert!( + !existing_install_is_valid(&final_path, &marker, bytes.len() as u64 + 1), + "a marker from the wrapper-as-CLI regression must not validate the full CLI" + ); + fs::write(&final_path, &bytes[..bytes.len() / 2]).expect("truncate"); + assert!( + !existing_install_is_valid(&final_path, &marker, bytes.len() as u64), + "a truncated binary must be detected via the size marker" + ); + + // Zero-length binary (quarantined to empty). + fs::write(&final_path, b"").expect("empty"); + assert!(!existing_install_is_valid( + &final_path, + &marker, + bytes.len() as u64 + )); +} + +#[test] +fn invalid_image_header_is_rejected() { + let dir = tempfile::tempdir().expect("tempdir"); + let final_path = dir.path().join("copilot-bin"); + let marker = marker_path(dir.path()); + + // Right size, has a marker, but the bytes are not a valid image. + let garbage = vec![0u8; 4096]; + fs::write(&final_path, &garbage).expect("write garbage"); + write_marker(&marker, garbage.len() as u64).expect("marker"); + + assert!( + !existing_install_is_valid(&final_path, &marker, garbage.len() as u64), + "a non-executable image must be rejected even with a matching marker" + ); +} + +#[test] +fn verification_rejects_size_and_content_mismatch() { + let dir = tempfile::tempdir().expect("tempdir"); + let path = dir.path().join("staged"); + let expected = fake_image(1024); + + // Exact match passes. + fs::write(&path, &expected).expect("write"); + verify_on_disk_matches(&path, &expected).expect("exact match should verify"); + + // Truncated -> size mismatch. + fs::write(&path, &expected[..100]).expect("truncate"); + assert!(verify_on_disk_matches(&path, &expected).is_err()); + + // Same length, different bytes -> content mismatch. + let mut tampered = expected.clone(); + *tampered.last_mut().expect("non-empty") ^= 0xFF; + fs::write(&path, &tampered).expect("tamper"); + assert!(verify_on_disk_matches(&path, &expected).is_err()); + + // Missing file -> I/O error. + fs::remove_file(&path).expect("remove"); + assert!(verify_on_disk_matches(&path, &expected).is_err()); +} + +#[test] +fn temp_files_are_unique_and_synced() { + let dir = tempfile::tempdir().expect("tempdir"); + let data = fake_image(256); + + let a = write_temp_file(dir.path(), &data).expect("temp a"); + let b = write_temp_file(dir.path(), &data).expect("temp b"); + + assert_ne!(a, b, "temp file names must be unique"); + assert_eq!(fs::read(&a).expect("read a"), data); + assert_eq!(fs::read(&b).expect("read b"), data); + + #[cfg(unix)] + { + use std::os::unix::fs::PermissionsExt; + let mode = fs::metadata(&a).expect("meta").permissions().mode(); + assert_eq!(mode & 0o777, 0o755, "temp binary should be executable"); + } +} + +#[cfg(has_bundled_cli)] +#[test] +fn runtime_install_replaces_stale_pair() { + let dir = tempfile::tempdir().expect("tempdir"); + fs::write(dir.path().join(RUNTIME_NODE_NAME), b"stale runtime").expect("seed runtime"); + fs::write(dir.path().join(RUNTIME_BINARY_NAME), b"stale wrapper").expect("seed wrapper"); + + install_runtime(dir.path(), build_time::RUNTIME_ARCHIVE).expect("install runtime"); + + assert_eq!( + fs::read(dir.path().join(RUNTIME_NODE_NAME)).expect("read runtime"), + extract_binary(build_time::RUNTIME_ARCHIVE, RUNTIME_NODE_NAME).expect("extract runtime") + ); + assert_eq!( + fs::read(dir.path().join(RUNTIME_BINARY_NAME)).expect("read wrapper"), + extract_binary(build_time::RUNTIME_ARCHIVE, RUNTIME_BINARY_NAME).expect("extract wrapper") + ); +} + +#[cfg(has_bundled_cli)] +#[test] +fn custom_runtime_install_dir_isolated_by_version() { + let dir = tempfile::tempdir().expect("tempdir"); + + assert_eq!( + runtime_install_dir(dir.path(), "1.0.0").expect("claim directory"), + dir.path() + ); + assert_eq!( + runtime_install_dir(dir.path(), "1.0.0").expect("reuse directory"), + dir.path() + ); + assert_eq!( + runtime_install_dir(dir.path(), "2.0.0").expect("isolate directory"), + dir.path().join("2.0.0") + ); +} + +#[cfg(has_bundled_cli)] +fn runtime_fixture(extra: &[(&str, &[u8], u32)]) -> Vec { + let encoder = flate2::write::GzEncoder::new(Vec::new(), flate2::Compression::fast()); + let mut archive = tar::Builder::new(encoder); + let mut entries = vec![ + (RUNTIME_BINARY_NAME, b"wrapper".as_slice(), 0o755), + (RUNTIME_NODE_NAME, b"runtime".as_slice(), 0o755), + ]; + #[cfg(feature = "in-process")] + entries.push((RUNTIME_LIBRARY_NAME, b"library".as_slice(), 0o644)); + entries.extend_from_slice(extra); + for (name, bytes, mode) in entries { + let mut header = tar::Header::new_gnu(); + // Raw names also let the installer see traversal fixtures which + // Builder::append_data would reject before reaching product code. + header.as_mut_bytes()[..name.len()].copy_from_slice(name.as_bytes()); + header.set_size(bytes.len() as u64); + header.set_mode(mode); + header.set_cksum(); + archive.append(&header, bytes).expect("append fixture"); + } + archive.into_inner().unwrap().finish().unwrap() +} + +#[cfg(has_bundled_cli)] +#[test] +fn warm_runtime_rejects_same_size_corruption_despite_unchanged_metadata() { + let dir = tempfile::tempdir().unwrap(); + let fixture = runtime_fixture(&[]); + install_runtime(dir.path(), &fixture).unwrap(); + let runtime = dir.path().join(RUNTIME_NODE_NAME); + let original = fs::metadata(&runtime).unwrap(); + fs::write(&runtime, b"corrupt").unwrap(); + fs::File::options() + .write(true) + .open(&runtime) + .unwrap() + .set_times(fs::FileTimes::new().set_modified(original.modified().unwrap())) + .unwrap(); + assert!(install_runtime(dir.path(), b"invalid archive").is_err()); + assert_eq!(fs::read(&runtime).unwrap(), b"corrupt"); + install_runtime(dir.path(), &fixture).unwrap(); + assert_eq!(fs::read(&runtime).unwrap(), b"runtime"); +} + +#[cfg(has_bundled_cli)] +fn assert_no_runtime_temps(dir: &Path) { + for entry in fs::read_dir(dir).unwrap() { + let entry = entry.unwrap(); + assert!( + !entry + .file_name() + .to_string_lossy() + .starts_with(".copilot-cli.tmp.") + ); + if entry.file_type().unwrap().is_dir() { + assert_no_runtime_temps(&entry.path()); + } + } +} + +#[cfg(has_bundled_cli)] +#[test] +fn runtime_cold_install_and_warm_reuse_preserve_bytes_and_modes() { + let dir = tempfile::tempdir().unwrap(); + let data = vec![0xAB; 3 * 64 * 1024 + 17]; + let archive = runtime_fixture(&[("nested/asset", &data, 0o640)]); + let wrapper = install_runtime(dir.path(), &archive).unwrap(); + assert_eq!(wrapper, dir.path().join(RUNTIME_BINARY_NAME)); + let asset = dir.path().join("nested/asset"); + assert_eq!(fs::read(&asset).unwrap(), data); + let modified = std::time::UNIX_EPOCH + std::time::Duration::from_secs(1234567890); + fs::File::options() + .write(true) + .open(&asset) + .unwrap() + .set_times(fs::FileTimes::new().set_modified(modified)) + .unwrap(); + + install_runtime(dir.path(), &archive).unwrap(); + + assert_eq!(fs::metadata(&asset).unwrap().modified().unwrap(), modified); + assert_eq!(fs::read(&asset).unwrap(), data); + #[cfg(unix)] + { + use std::os::unix::fs::PermissionsExt; + assert_eq!( + fs::metadata(&asset).unwrap().permissions().mode() & 0o777, + 0o640 + ); + assert_eq!( + fs::metadata(wrapper).unwrap().permissions().mode() & 0o777, + 0o755 + ); + } + assert_no_runtime_temps(dir.path()); +} + +#[cfg(has_bundled_cli)] +#[test] +fn runtime_repairs_same_size_corruption_truncation_and_extra_bytes() { + let dir = tempfile::tempdir().unwrap(); + let archive = runtime_fixture(&[]); + let runtime = dir.path().join(RUNTIME_NODE_NAME); + install_runtime(dir.path(), &archive).unwrap(); + + for corrupt in [b"runtimX".as_slice(), b"run", b"", b"runtime plus garbage"] { + fs::write(&runtime, corrupt).unwrap(); + install_runtime(dir.path(), &archive).unwrap(); + assert_eq!(fs::read(&runtime).unwrap(), b"runtime"); + assert_no_runtime_temps(dir.path()); + } +} + +#[cfg(has_bundled_cli)] +#[test] +fn runtime_repairs_corruption_across_comparison_chunks() { + let dir = tempfile::tempdir().unwrap(); + let bytes = vec![0xAB; 3 * 64 * 1024 + 17]; + let archive = runtime_fixture(&[("nested/asset", &bytes, 0o644)]); + install_runtime(dir.path(), &archive).unwrap(); + for offset in [0, bytes.len() / 2, bytes.len() - 1] { + let mut corrupt = bytes.clone(); + corrupt[offset] ^= 1; + fs::write(dir.path().join("nested/asset"), &corrupt).unwrap(); + install_runtime(dir.path(), &archive).unwrap(); + assert_eq!(fs::read(dir.path().join("nested/asset")).unwrap(), bytes); + assert_no_runtime_temps(dir.path()); + } +} + +#[cfg(all(has_bundled_cli, unix))] +#[test] +fn runtime_reuse_preserves_executable_caller_selected_permissions() { + use std::os::unix::fs::PermissionsExt; + + let dir = tempfile::tempdir().unwrap(); + let fixture = runtime_fixture(&[]); + let wrapper = install_runtime(dir.path(), &fixture).unwrap(); + for mode in [0o745, 0o754, 0o700, 0o500] { + fs::set_permissions(&wrapper, fs::Permissions::from_mode(mode)).unwrap(); + install_runtime(dir.path(), &fixture).unwrap(); + assert_eq!( + fs::metadata(&wrapper).unwrap().permissions().mode() & 0o777, + mode + ); + } + assert_no_runtime_temps(dir.path()); +} + +#[cfg(all(has_bundled_cli, unix))] +#[test] +fn runtime_repairs_nonexecutable_wrapper() { + use std::os::unix::fs::PermissionsExt; + + let dir = tempfile::tempdir().unwrap(); + let archive = runtime_fixture(&[]); + let wrapper = install_runtime(dir.path(), &archive).unwrap(); + for mode in [0o400, 0o600] { + fs::set_permissions(&wrapper, fs::Permissions::from_mode(mode)).unwrap(); + install_runtime(dir.path(), &archive).unwrap(); + assert_eq!( + fs::metadata(&wrapper).unwrap().permissions().mode() & 0o777, + 0o755 + ); + assert_eq!(fs::read(&wrapper).unwrap(), b"wrapper"); + assert_no_runtime_temps(dir.path()); + } +} + +#[cfg(all(has_bundled_cli, unix))] +#[test] +fn runtime_rejects_nonexecutable_wrapper_in_readonly_cache() { + use std::os::unix::fs::PermissionsExt; + + let dir = tempfile::tempdir().unwrap(); + let archive = runtime_fixture(&[]); + let wrapper = install_runtime(dir.path(), &archive).unwrap(); + fs::set_permissions(&wrapper, fs::Permissions::from_mode(0o400)).unwrap(); + fs::set_permissions(dir.path(), fs::Permissions::from_mode(0o555)).unwrap(); + let write_denied = fs::File::create(dir.path().join("write-probe")).is_err(); + let result = install_runtime(dir.path(), &archive); + fs::set_permissions(dir.path(), fs::Permissions::from_mode(0o755)).unwrap(); + if write_denied { + assert!( + result.is_err(), + "returned a nonexecutable wrapper: {result:?}" + ); + assert_eq!( + fs::metadata(&wrapper).unwrap().permissions().mode() & 0o777, + 0o400 + ); + } else { + eprintln!("read-only permission enforcement unavailable (e.g. privileged user)"); + fs::remove_file(dir.path().join("write-probe")).unwrap(); + result.unwrap(); + assert_eq!( + fs::metadata(&wrapper).unwrap().permissions().mode() & 0o777, + 0o755 + ); + } + assert_no_runtime_temps(dir.path()); +} + +#[cfg(has_bundled_cli)] +#[test] +fn runtime_archive_errors_do_not_publish_and_clean_up_staging() { + let dir = tempfile::tempdir().unwrap(); + let valid = runtime_fixture(&[("nested/asset", b"asset", 0o644)]); + let mut invalid_crc = valid.clone(); + let crc = invalid_crc.len() - 8; + invalid_crc[crc] ^= 0xFF; + let mut invalid_length = valid.clone(); + let length = invalid_length.len() - 4; + invalid_length[length] ^= 0xFF; + let mut truncated_trailer = valid.clone(); + truncated_trailer.truncate(valid.len() - 1); + let mut truncated_body = valid.clone(); + truncated_body.truncate(valid.len() / 2); + for archive in [ + invalid_crc, + invalid_length, + truncated_trailer, + truncated_body, + ] { + let runtime = dir.path().join(RUNTIME_NODE_NAME); + fs::write(&runtime, b"previous complete runtime").unwrap(); + assert!(install_runtime(dir.path(), &archive).is_err()); + assert_eq!(fs::read(runtime).unwrap(), b"previous complete runtime"); + assert!(!dir.path().join(RUNTIME_BINARY_NAME).exists()); + assert!(!dir.path().join("nested/asset").exists()); + assert_no_runtime_temps(dir.path()); + } +} + +#[cfg(has_bundled_cli)] +#[test] +fn runtime_short_entry_is_rejected_without_publishing() { + let dir = tempfile::tempdir().unwrap(); + let mut header = tar::Header::new_gnu(); + header.set_path(RUNTIME_NODE_NAME).unwrap(); + header.set_size(128 * 1024); + header.set_mode(0o755); + header.set_cksum(); + let mut encoder = flate2::write::GzEncoder::new(Vec::new(), flate2::Compression::fast()); + encoder.write_all(header.as_bytes()).unwrap(); + encoder.write_all(b"short").unwrap(); + let archive = encoder.finish().unwrap(); + + assert!(install_runtime(dir.path(), &archive).is_err()); + assert!(!dir.path().join(RUNTIME_NODE_NAME).exists()); + assert_no_runtime_temps(dir.path()); +} + +#[cfg(has_bundled_cli)] +#[test] +fn runtime_missing_required_entry_is_rejected_before_publish() { + let dir = tempfile::tempdir().unwrap(); + let encoder = flate2::write::GzEncoder::new(Vec::new(), flate2::Compression::fast()); + let mut archive = tar::Builder::new(encoder); + let mut header = tar::Header::new_gnu(); + header.set_size(7); + header.set_mode(0o755); + header.set_cksum(); + archive + .append_data(&mut header, RUNTIME_BINARY_NAME, b"wrapper".as_slice()) + .unwrap(); + let archive = archive.into_inner().unwrap().finish().unwrap(); + + assert!(install_runtime(dir.path(), &archive).is_err()); + assert!(!dir.path().join(RUNTIME_BINARY_NAME).exists()); + assert_no_runtime_temps(dir.path()); +} + +#[cfg(has_bundled_cli)] +#[test] +fn runtime_rejects_traversal_and_cleans_preceding_entries() { + let dir = tempfile::tempdir().unwrap(); + let install_dir = dir.path().join("install"); + let archive = runtime_fixture(&[("../escaped", b"bad", 0o644)]); + assert!(install_runtime(&install_dir, &archive).is_err()); + assert!(!dir.path().join("escaped").exists()); + assert!(!install_dir.join(RUNTIME_BINARY_NAME).exists()); + assert_no_runtime_temps(&install_dir); +} + +#[cfg(all(has_bundled_cli, unix))] +#[test] +fn runtime_rejects_symlink_parents_and_targets() { + use std::os::unix::fs::symlink; + + let dir = tempfile::tempdir().unwrap(); + let outside = tempfile::tempdir().unwrap(); + fs::write(outside.path().join("asset"), b"outside").unwrap(); + symlink(outside.path(), dir.path().join("nested")).unwrap(); + let archive = runtime_fixture(&[("nested/asset", b"new", 0o644)]); + + assert!(install_runtime(dir.path(), &archive).is_err()); + assert_eq!(fs::read(outside.path().join("asset")).unwrap(), b"outside"); + assert_no_runtime_temps(dir.path()); + fs::remove_file(dir.path().join("nested")).unwrap(); + symlink( + outside.path().join("asset"), + dir.path().join(RUNTIME_NODE_NAME), + ) + .unwrap(); + assert!(install_runtime(dir.path(), &archive).is_err()); + assert_eq!(fs::read(outside.path().join("asset")).unwrap(), b"outside"); + assert_no_runtime_temps(dir.path()); +} + +#[cfg(has_bundled_cli)] +#[test] +fn concurrent_runtime_installers_publish_complete_files() { + let dir = tempfile::tempdir().unwrap(); + let data = vec![0xAB; 256 * 1024 + 1]; + let archive = runtime_fixture(&[("nested/asset", &data, 0o644)]); + let barrier = std::sync::Barrier::new(6); + std::thread::scope(|scope| { + for _ in 0..6 { + scope.spawn(|| { + barrier.wait(); + install_runtime(dir.path(), &archive).unwrap(); + }); + } + }); + assert_eq!(fs::read(dir.path().join("nested/asset")).unwrap(), data); + assert_eq!( + fs::read(dir.path().join(RUNTIME_NODE_NAME)).unwrap(), + b"runtime" + ); + assert_no_runtime_temps(dir.path()); +} + +#[cfg(has_bundled_cli)] +#[test] +fn runtime_duplicate_destinations_fail_on_cold_and_warm_installs() { + for name in ["asset", "./asset"] { + let dir = tempfile::tempdir().unwrap(); + let archive = runtime_fixture(&[("asset", b"first", 0o644), (name, b"last", 0o644)]); + assert!(install_runtime(dir.path(), &archive).is_err()); + assert!(!dir.path().join("asset").exists()); + assert_no_runtime_temps(dir.path()); + + fs::write(dir.path().join("asset"), b"last").unwrap(); + assert!(install_runtime(dir.path(), &archive).is_err()); + assert_eq!(fs::read(dir.path().join("asset")).unwrap(), b"last"); + assert_no_runtime_temps(dir.path()); + } + let dir = tempfile::tempdir().unwrap(); + let archive = runtime_fixture(&[(RUNTIME_NODE_NAME, b"", 0o755)]); + assert!(install_runtime(dir.path(), &archive).is_err()); + assert!(!dir.path().join(RUNTIME_NODE_NAME).exists()); + assert_no_runtime_temps(dir.path()); + install_runtime(dir.path(), &runtime_fixture(&[])).unwrap(); + assert!(install_runtime(dir.path(), &archive).is_err()); + assert_eq!( + fs::read(dir.path().join(RUNTIME_NODE_NAME)).unwrap(), + b"runtime" + ); + assert_no_runtime_temps(dir.path()); +} + +#[cfg(has_bundled_cli)] +#[test] +fn runtime_case_aliases_cannot_overwrite_required_artifacts() { + let names = [ + RUNTIME_NODE_NAME, + RUNTIME_BINARY_NAME, + #[cfg(feature = "in-process")] + RUNTIME_LIBRARY_NAME, + ]; + for name in names { + let alias = name.to_ascii_uppercase(); + let archive = runtime_fixture(&[(&alias, b"", 0o755)]); + let dir = tempfile::tempdir().unwrap(); + let result = install_runtime(dir.path(), &archive); + assert!(result.is_err(), "accepted alias {alias}: {result:?}"); + assert!(!dir.path().join(name).exists()); + assert_no_runtime_temps(dir.path()); + + install_runtime(dir.path(), &runtime_fixture(&[])).unwrap(); + let original = fs::read(dir.path().join(name)).unwrap(); + assert!(install_runtime(dir.path(), &archive).is_err()); + assert_eq!(fs::read(dir.path().join(name)).unwrap(), original); + fs::write(dir.path().join(name), b"corrupt").unwrap(); + assert!(install_runtime(dir.path(), &archive).is_err()); + assert_eq!(fs::read(dir.path().join(name)).unwrap(), b"corrupt"); + assert_no_runtime_temps(dir.path()); + } +} + +#[cfg(has_bundled_cli)] +#[test] +fn runtime_mixed_separator_and_case_aliases_are_rejected() { + let dir = tempfile::tempdir().unwrap(); + let archive = runtime_fixture(&[ + ("nested/asset", b"first", 0o644), + (r".\NESTED\ASSET", b"last", 0o644), + ]); + assert!(install_runtime(dir.path(), &archive).is_err()); + assert!(!dir.path().join("nested/asset").exists()); + assert_no_runtime_temps(dir.path()); + + install_runtime( + dir.path(), + &runtime_fixture(&[("nested/asset", b"last", 0o644)]), + ) + .unwrap(); + assert!(install_runtime(dir.path(), &archive).is_err()); + assert_eq!(fs::read(dir.path().join("nested/asset")).unwrap(), b"last"); + assert_no_runtime_temps(dir.path()); +} + +#[cfg(has_bundled_cli)] +#[test] +fn runtime_nonportable_alias_paths_are_rejected_before_publish() { + for name in [ + "runtime.node.", + "runtime.node ", + "runtime.node:stream", + "RUNTIM~1.NOD", + "NUL", + "con.txt", + "aux .txt", + "COM1", + "LPT9.txt", + "n\u{00e9}sted/asset", + r"..\escaped", + r"C:\escaped", + r"\\server\share\asset", + ] { + let dir = tempfile::tempdir().unwrap(); + let archive = runtime_fixture(&[(name, b"bad", 0o644)]); + assert!( + install_runtime(dir.path(), &archive).is_err(), + "accepted {name}" + ); + assert!(!dir.path().join(RUNTIME_NODE_NAME).exists()); + assert_no_runtime_temps(dir.path()); + } +} + +#[cfg(all(has_bundled_cli, feature = "in-process"))] +#[test] +fn runtime_library_aliases_are_rejected_before_repair() { + let alias = format!("./{RUNTIME_LIBRARY_NAME}"); + for duplicate in [b"another".as_slice(), b""] { + let dir = tempfile::tempdir().unwrap(); + let archive = runtime_fixture(&[(&alias, duplicate, 0o644)]); + assert!(install_runtime(dir.path(), &archive).is_err()); + assert!(!dir.path().join(RUNTIME_LIBRARY_NAME).exists()); + assert_no_runtime_temps(dir.path()); + + install_runtime(dir.path(), &runtime_fixture(&[])).unwrap(); + let library = dir.path().join(RUNTIME_LIBRARY_NAME); + assert!(install_runtime(dir.path(), &archive).is_err()); + assert_eq!(fs::read(&library).unwrap(), b"library"); + fs::write(&library, b"corrupt").unwrap(); + assert!(install_runtime(dir.path(), &archive).is_err()); + assert_eq!(fs::read(&library).unwrap(), b"corrupt"); + assert_no_runtime_temps(dir.path()); + } +} + +#[cfg(all(has_bundled_cli, unix))] +#[test] +fn warm_runtime_install_needs_no_writable_cache() { + assert_read_only_runtime_cache(0o555, false); +} + +#[cfg(all(has_bundled_cli, unix))] +#[test] +fn warm_runtime_reuses_owner_only_immutable_cache() { + assert_read_only_runtime_cache(0o500, false); +} + +#[cfg(all(has_bundled_cli, unix))] +#[test] +fn warm_runtime_reuses_nonexecutable_native_library() { + assert_read_only_runtime_cache(0o555, true); + assert_read_only_runtime_cache(0o500, true); +} + +#[cfg(all(has_bundled_cli, unix))] +fn assert_read_only_runtime_cache(permission_mask: u32, readonly_native_library: bool) { + use std::os::unix::fs::PermissionsExt; + + let dir = tempfile::tempdir().unwrap(); + let archive = runtime_fixture(&[("nested/asset", b"asset", 0o644)]); + install_runtime(dir.path(), &archive).unwrap(); + let files = [ + RUNTIME_BINARY_NAME, + RUNTIME_NODE_NAME, + "nested/asset", + #[cfg(feature = "in-process")] + RUNTIME_LIBRARY_NAME, + ]; + let mut read_only_files = Vec::new(); + for name in files { + let path = dir.path().join(name); + let mut mode = fs::metadata(&path).unwrap().permissions().mode() & permission_mask; + if readonly_native_library && name != RUNTIME_BINARY_NAME { + mode &= !0o111; + } + fs::set_permissions(&path, fs::Permissions::from_mode(mode)).unwrap(); + read_only_files.push((path, mode)); + } + fs::set_permissions( + dir.path().join("nested"), + fs::Permissions::from_mode(permission_mask), + ) + .unwrap(); + fs::set_permissions(dir.path(), fs::Permissions::from_mode(permission_mask)).unwrap(); + let write_denied = fs::File::create(dir.path().join("write-probe")).is_err(); + let result = install_runtime(dir.path(), &archive); + fs::set_permissions(dir.path(), fs::Permissions::from_mode(0o755)).unwrap(); + fs::set_permissions(dir.path().join("nested"), fs::Permissions::from_mode(0o755)).unwrap(); + result.unwrap(); + if !write_denied { + eprintln!("read-only permission enforcement unavailable (e.g. privileged user)"); + fs::remove_file(dir.path().join("write-probe")).unwrap(); + } + for (path, mode) in read_only_files { + assert_eq!( + fs::metadata(path).unwrap().permissions().mode() & 0o777, + mode + ); + } + assert_eq!(fs::read(dir.path().join("nested/asset")).unwrap(), b"asset"); + assert_no_runtime_temps(dir.path()); +} + +#[cfg(all(has_bundled_cli, unix))] +#[test] +fn runtime_repairs_unreadable_but_replaceable_file() { + use std::os::unix::fs::PermissionsExt; + + let dir = tempfile::tempdir().unwrap(); + let archive = runtime_fixture(&[("asset", b"trusted", 0o000)]); + let asset = dir.path().join("asset"); + fs::write(&asset, b"corrupt").unwrap(); + fs::set_permissions(&asset, fs::Permissions::from_mode(0o000)).unwrap(); + if fs::File::open(&asset).is_ok() { + eprintln!("unreadable permission enforcement unavailable (e.g. privileged user)"); + } + let result = install_runtime(dir.path(), &archive); + let mode = fs::metadata(&asset).unwrap().permissions().mode() & 0o777; + fs::set_permissions(&asset, fs::Permissions::from_mode(0o600)).unwrap(); + result.unwrap(); + assert_eq!(mode, 0o000); + assert_eq!(fs::read(asset).unwrap(), b"trusted"); + assert_no_runtime_temps(dir.path()); +} + +#[cfg(has_bundled_cli)] +#[test] +fn runtime_replacement_preserves_open_reader_contents() { + let dir = tempfile::tempdir().unwrap(); + let asset = dir.path().join("asset"); + let original = runtime_fixture(&[("asset", b"old complete file", 0o644)]); + install_runtime(dir.path(), &original).unwrap(); + let mut reader = fs::File::open(&asset).unwrap(); + let replacement = runtime_fixture(&[("asset", b"new complete file", 0o644)]); + install_runtime(dir.path(), &replacement).unwrap(); + let mut bytes = Vec::new(); + reader.read_to_end(&mut bytes).unwrap(); + assert_eq!(bytes, b"old complete file"); + assert_eq!(fs::read(&asset).unwrap(), b"new complete file"); + assert_no_runtime_temps(dir.path()); +} + +#[test] +fn failed_publish_does_not_remove_previous_file() { + let dir = tempfile::tempdir().unwrap(); + let target = dir.path().join("installed"); + fs::write(&target, b"previous complete file").unwrap(); + assert!(publish(&dir.path().join("missing-temporary"), &target).is_err()); + assert_eq!(fs::read(target).unwrap(), b"previous complete file"); +} diff --git a/rust/src/ffi.rs b/rust/src/ffi.rs index bbd4138099..df0269b0b8 100644 --- a/rust/src/ffi.rs +++ b/rust/src/ffi.rs @@ -594,154 +594,7 @@ fn build_env_json(environment: &[(String, String)]) -> Option> { } #[cfg(test)] -mod tests { - use std::sync::Mutex; - use std::sync::atomic::AtomicUsize; - use std::time::{Duration, Instant}; - - use super::*; - - static FFI_LIFECYCLE_TEST_LOCK: Mutex<()> = Mutex::new(()); - static TEST_ALLOW_CLOSE: AtomicBool = AtomicBool::new(false); - static TEST_CLOSE_CALLS: AtomicUsize = AtomicUsize::new(0); - static TEST_SHUTDOWN_CALLS: AtomicUsize = AtomicUsize::new(0); - - unsafe extern "C" fn test_host_shutdown(_server_id: u32) -> bool { - TEST_SHUTDOWN_CALLS.fetch_add(1, Ordering::SeqCst); - true - } - - unsafe extern "C" fn test_connection_write( - _connection_id: u32, - _bytes: *const u8, - _length: usize, - ) -> bool { - true - } - - unsafe extern "C" fn test_connection_close(_connection_id: u32) -> bool { - TEST_CLOSE_CALLS.fetch_add(1, Ordering::SeqCst); - TEST_ALLOW_CLOSE.load(Ordering::SeqCst) - } - - #[test] - fn argv_without_entrypoint_contains_only_client_options() { - let argv: Vec = serde_json::from_slice(&build_argv_json( - None, - &["--log-level".into(), "debug".into()], - )) - .unwrap(); - - assert_eq!(argv, ["--log-level", "debug"]); - } - - #[test] - fn explicit_javascript_entrypoint_uses_node() { - let argv: Vec = - serde_json::from_slice(&build_argv_json(Some(Path::new("index.js")), &[])).unwrap(); - - assert_eq!( - argv, - ["node", "index.js", "--embedded-host", "--no-auto-update"] - ); - } - - #[cfg(windows)] - #[test] - fn child_process_path_removes_windows_verbatim_prefix() { - assert_eq!( - path_for_child_process(PathBuf::from(r"\\?\D:\a\copilot-sdk\index.js")), - PathBuf::from(r"D:\a\copilot-sdk\index.js") - ); - assert_eq!( - path_for_child_process(PathBuf::from(r"\\?\UNC\server\share\copilot-sdk\index.js")), - PathBuf::from(r"\\server\share\copilot-sdk\index.js") - ); - } - - #[test] - fn environment_is_omitted_when_empty() { - assert_eq!(build_env_json(&[]), None); - } - - #[test] - fn environment_serializes_worker_overrides() { - let env: serde_json::Value = serde_json::from_slice( - &build_env_json(&[ - ("COPILOT_HOME".into(), "state".into()), - ("COPILOT_DISABLE_KEYTAR".into(), "1".into()), - ]) - .unwrap(), - ) - .unwrap(); - - assert_eq!( - env, - serde_json::json!({ - "COPILOT_HOME": "state", - "COPILOT_DISABLE_KEYTAR": "1", - }) - ); - } - - #[test] - fn callback_state_is_retained_until_connection_close_succeeds() { - let _guard = FFI_LIFECYCLE_TEST_LOCK.lock().unwrap(); - TEST_ALLOW_CLOSE.store(false, Ordering::SeqCst); - TEST_CLOSE_CALLS.store(0, Ordering::SeqCst); - TEST_SHUTDOWN_CALLS.store(0, Ordering::SeqCst); - - let (tx, mut rx) = mpsc::unbounded_channel(); - let state_ptr = Box::into_raw(Box::new(CallbackState { - tx, - closing: AtomicBool::new(false), - })); - let shared = FfiShared { - host_shutdown: test_host_shutdown, - connection_write: test_connection_write, - connection_close: test_connection_close, - server_id: AtomicU32::new(11), - connection_id: AtomicU32::new(21), - callback_state: AtomicPtr::new(state_ptr), - closed: AtomicBool::new(false), - operation_lock: parking_lot::Mutex::new(()), - library_path: PathBuf::from("test-runtime"), - }; - - shared.close(); - - assert!(TEST_CLOSE_CALLS.load(Ordering::SeqCst) >= 1); - assert_eq!(TEST_SHUTDOWN_CALLS.load(Ordering::SeqCst), 0); - assert_eq!(shared.connection_id.load(Ordering::SeqCst), 0); - assert!(shared.callback_state.load(Ordering::SeqCst).is_null()); - - assert!(matches!( - rx.try_recv(), - Err(mpsc::error::TryRecvError::Empty) - )); - - TEST_ALLOW_CLOSE.store(true, Ordering::SeqCst); - let deadline = Instant::now() + Duration::from_secs(5); - while Instant::now() < deadline && TEST_SHUTDOWN_CALLS.load(Ordering::SeqCst) == 0 { - std::thread::sleep(Duration::from_millis(10)); - } - - assert!(TEST_CLOSE_CALLS.load(Ordering::SeqCst) >= 2); - assert_eq!(TEST_SHUTDOWN_CALLS.load(Ordering::SeqCst), 1); - assert!(matches!( - rx.try_recv(), - Err(mpsc::error::TryRecvError::Disconnected) - )); - - let close_calls_after_cleanup = TEST_CLOSE_CALLS.load(Ordering::SeqCst); - shared.close(); - assert_eq!( - TEST_CLOSE_CALLS.load(Ordering::SeqCst), - close_calls_after_cleanup - ); - assert_eq!(TEST_SHUTDOWN_CALLS.load(Ordering::SeqCst), 1); - } -} +mod tests; #[cfg(test)] mod shutdown_tests; diff --git a/rust/src/ffi/tests.rs b/rust/src/ffi/tests.rs new file mode 100644 index 0000000000..5662dc4e5b --- /dev/null +++ b/rust/src/ffi/tests.rs @@ -0,0 +1,152 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +#![cfg(test)] + +use std::sync::Mutex; +use std::sync::atomic::AtomicUsize; +use std::time::{Duration, Instant}; + +use super::*; + +static FFI_LIFECYCLE_TEST_LOCK: Mutex<()> = Mutex::new(()); +static TEST_ALLOW_CLOSE: AtomicBool = AtomicBool::new(false); +static TEST_CLOSE_CALLS: AtomicUsize = AtomicUsize::new(0); +static TEST_SHUTDOWN_CALLS: AtomicUsize = AtomicUsize::new(0); + +unsafe extern "C" fn test_host_shutdown(_server_id: u32) -> bool { + TEST_SHUTDOWN_CALLS.fetch_add(1, Ordering::SeqCst); + true +} + +unsafe extern "C" fn test_connection_write( + _connection_id: u32, + _bytes: *const u8, + _length: usize, +) -> bool { + true +} + +unsafe extern "C" fn test_connection_close(_connection_id: u32) -> bool { + TEST_CLOSE_CALLS.fetch_add(1, Ordering::SeqCst); + TEST_ALLOW_CLOSE.load(Ordering::SeqCst) +} + +#[test] +fn argv_without_entrypoint_contains_only_client_options() { + let argv: Vec = serde_json::from_slice(&build_argv_json( + None, + &["--log-level".into(), "debug".into()], + )) + .unwrap(); + + assert_eq!(argv, ["--log-level", "debug"]); +} + +#[test] +fn explicit_javascript_entrypoint_uses_node() { + let argv: Vec = + serde_json::from_slice(&build_argv_json(Some(Path::new("index.js")), &[])).unwrap(); + + assert_eq!( + argv, + ["node", "index.js", "--embedded-host", "--no-auto-update"] + ); +} + +#[cfg(windows)] +#[test] +fn child_process_path_removes_windows_verbatim_prefix() { + assert_eq!( + path_for_child_process(PathBuf::from(r"\\?\D:\a\copilot-sdk\index.js")), + PathBuf::from(r"D:\a\copilot-sdk\index.js") + ); + assert_eq!( + path_for_child_process(PathBuf::from(r"\\?\UNC\server\share\copilot-sdk\index.js")), + PathBuf::from(r"\\server\share\copilot-sdk\index.js") + ); +} + +#[test] +fn environment_is_omitted_when_empty() { + assert_eq!(build_env_json(&[]), None); +} + +#[test] +fn environment_serializes_worker_overrides() { + let env: serde_json::Value = serde_json::from_slice( + &build_env_json(&[ + ("COPILOT_HOME".into(), "state".into()), + ("COPILOT_DISABLE_KEYTAR".into(), "1".into()), + ]) + .unwrap(), + ) + .unwrap(); + + assert_eq!( + env, + serde_json::json!({ + "COPILOT_HOME": "state", + "COPILOT_DISABLE_KEYTAR": "1", + }) + ); +} + +#[test] +fn callback_state_is_retained_until_connection_close_succeeds() { + let _guard = FFI_LIFECYCLE_TEST_LOCK.lock().unwrap(); + TEST_ALLOW_CLOSE.store(false, Ordering::SeqCst); + TEST_CLOSE_CALLS.store(0, Ordering::SeqCst); + TEST_SHUTDOWN_CALLS.store(0, Ordering::SeqCst); + + let (tx, mut rx) = mpsc::unbounded_channel(); + let state_ptr = Box::into_raw(Box::new(CallbackState { + tx, + closing: AtomicBool::new(false), + })); + let shared = FfiShared { + host_shutdown: test_host_shutdown, + connection_write: test_connection_write, + connection_close: test_connection_close, + server_id: AtomicU32::new(11), + connection_id: AtomicU32::new(21), + callback_state: AtomicPtr::new(state_ptr), + closed: AtomicBool::new(false), + operation_lock: parking_lot::Mutex::new(()), + library_path: PathBuf::from("test-runtime"), + }; + + shared.close(); + + assert!(TEST_CLOSE_CALLS.load(Ordering::SeqCst) >= 1); + assert_eq!(TEST_SHUTDOWN_CALLS.load(Ordering::SeqCst), 0); + assert_eq!(shared.connection_id.load(Ordering::SeqCst), 0); + assert!(shared.callback_state.load(Ordering::SeqCst).is_null()); + + assert!(matches!( + rx.try_recv(), + Err(mpsc::error::TryRecvError::Empty) + )); + + TEST_ALLOW_CLOSE.store(true, Ordering::SeqCst); + let deadline = Instant::now() + Duration::from_secs(5); + while Instant::now() < deadline && TEST_SHUTDOWN_CALLS.load(Ordering::SeqCst) == 0 { + std::thread::sleep(Duration::from_millis(10)); + } + + assert!(TEST_CLOSE_CALLS.load(Ordering::SeqCst) >= 2); + assert_eq!(TEST_SHUTDOWN_CALLS.load(Ordering::SeqCst), 1); + assert!(matches!( + rx.try_recv(), + Err(mpsc::error::TryRecvError::Disconnected) + )); + + let close_calls_after_cleanup = TEST_CLOSE_CALLS.load(Ordering::SeqCst); + shared.close(); + assert_eq!( + TEST_CLOSE_CALLS.load(Ordering::SeqCst), + close_calls_after_cleanup + ); + assert_eq!(TEST_SHUTDOWN_CALLS.load(Ordering::SeqCst), 1); +} diff --git a/rust/src/generated/api_types.rs b/rust/src/generated/api_types.rs index b06fad33d4..3532793c48 100644 --- a/rust/src/generated/api_types.rs +++ b/rust/src/generated/api_types.rs @@ -56,6 +56,16 @@ pub mod rpc_methods { pub const MODELS_GETBUILTINCATALOG: &str = "models.getBuiltInCatalog"; /// `sandbox.getHostSupport` pub const SANDBOX_GETHOSTSUPPORT: &str = "sandbox.getHostSupport"; + /// `sandbox.proxyCa.getStatus` + pub const SANDBOX_PROXYCA_GETSTATUS: &str = "sandbox.proxyCa.getStatus"; + /// `sandbox.proxyCa.create` + pub const SANDBOX_PROXYCA_CREATE: &str = "sandbox.proxyCa.create"; + /// `sandbox.proxyCa.rotate` + pub const SANDBOX_PROXYCA_ROTATE: &str = "sandbox.proxyCa.rotate"; + /// `sandbox.proxyCa.trust` + pub const SANDBOX_PROXYCA_TRUST: &str = "sandbox.proxyCa.trust"; + /// `sandbox.proxyCa.remove` + pub const SANDBOX_PROXYCA_REMOVE: &str = "sandbox.proxyCa.remove"; /// `tools.list` pub const TOOLS_LIST: &str = "tools.list"; /// `account.getQuota` @@ -272,8 +282,17 @@ pub mod rpc_methods { pub const SESSIONS_CONFIGURESESSIONEXTENSIONS: &str = "sessions.configureSessionExtensions"; /// `agentRegistry.spawn` pub const AGENTREGISTRY_SPAWN: &str = "agentRegistry.spawn"; - /// `accounts.acquireEntraToken` - pub const ACCOUNTS_ACQUIREENTRATOKEN: &str = "accounts.acquireEntraToken"; + /// `session.providers.getCatalog` + pub const SESSION_PROVIDERS_GETCATALOG: &str = "session.providers.getCatalog"; + /// `session.providers.discover` + pub const SESSION_PROVIDERS_DISCOVER: &str = "session.providers.discover"; + /// `session.providers.getStatus` + pub const SESSION_PROVIDERS_GETSTATUS: &str = "session.providers.getStatus"; + /// `session.providers.models.list` + pub const SESSION_PROVIDERS_MODELS_LIST: &str = "session.providers.models.list"; + /// `session.providers.models.prepareConfiguration` + pub const SESSION_PROVIDERS_MODELS_PREPARECONFIGURATION: &str = + "session.providers.models.prepareConfiguration"; /// `session.suspend` pub const SESSION_SUSPEND: &str = "session.suspend"; /// `session.send` @@ -719,8 +738,12 @@ pub mod rpc_methods { pub const SESSION_UI_ELICITATION: &str = "session.ui.elicitation"; /// `session.ui.handlePendingElicitation` pub const SESSION_UI_HANDLEPENDINGELICITATION: &str = "session.ui.handlePendingElicitation"; + /// `session.ui.handleHumanAskUser` + pub const SESSION_UI_HANDLEHUMANASKUSER: &str = "session.ui.handleHumanAskUser"; /// `session.ui.handlePendingUserInput` pub const SESSION_UI_HANDLEPENDINGUSERINPUT: &str = "session.ui.handlePendingUserInput"; + /// `session.ui.handleHumanUserInput` + pub const SESSION_UI_HANDLEHUMANUSERINPUT: &str = "session.ui.handleHumanUserInput"; /// `session.ui.handlePendingSampling` pub const SESSION_UI_HANDLEPENDINGSAMPLING: &str = "session.ui.handlePendingSampling"; /// `session.ui.handlePendingAutoModeSwitch` @@ -731,6 +754,8 @@ pub mod rpc_methods { "session.ui.handlePendingSessionLimitsExhausted"; /// `session.ui.handlePendingExitPlanMode` pub const SESSION_UI_HANDLEPENDINGEXITPLANMODE: &str = "session.ui.handlePendingExitPlanMode"; + /// `session.ui.handleHumanExitPlanMode` + pub const SESSION_UI_HANDLEHUMANEXITPLANMODE: &str = "session.ui.handleHumanExitPlanMode"; /// `session.ui.registerDirectAutoModeSwitchHandler` pub const SESSION_UI_REGISTERDIRECTAUTOMODESWITCHHANDLER: &str = "session.ui.registerDirectAutoModeSwitchHandler"; @@ -943,8 +968,12 @@ pub mod rpc_methods { pub const TASKS_CANCEL: &str = "tasks.cancel"; /// `sessionFs.readFile` pub const SESSIONFS_READFILE: &str = "sessionFs.readFile"; + /// `sessionFs.readFileBytes` + pub const SESSIONFS_READFILEBYTES: &str = "sessionFs.readFileBytes"; /// `sessionFs.writeFile` pub const SESSIONFS_WRITEFILE: &str = "sessionFs.writeFile"; + /// `sessionFs.writeFileBytes` + pub const SESSIONFS_WRITEFILEBYTES: &str = "sessionFs.writeFileBytes"; /// `sessionFs.appendFile` pub const SESSIONFS_APPENDFILE: &str = "sessionFs.appendFile"; /// `sessionFs.exists` @@ -6109,6 +6138,9 @@ pub struct CurrentModel { /// Latest unclaimed Auto preference waiting for a future user turn. Null means the pending request is returning to provider-default routing. #[serde(skip_serializing_if = "Option::is_none")] pub pending_auto_tier: Option, + /// Captured base model to restore when leaving plan mode. Omitted outside plan mode or when no plan override has captured a base model. Persistent agent model requirements apply to this model rather than the temporary plan model. + #[serde(skip_serializing_if = "Option::is_none")] + pub plan_base_model_id: Option, /// Reasoning effort level currently applied to the active model, when one is set. Reads `Session.getReasoningEffort()` synchronously after `getSelectedModel()` resolves so the two values are reported as a snapshot. #[serde(skip_serializing_if = "Option::is_none")] pub reasoning_effort: Option, @@ -6146,6 +6178,45 @@ pub struct CurrentToolMetadata { pub namespaced_name: Option, } +/// Result of one customization reload component. +/// +///
+/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
+#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct CustomizationReloadOutcome { + /// Reason for a skipped component or description of a failure, when available + #[serde(skip_serializing_if = "Option::is_none")] + pub detail: Option, + /// Whether the component reloaded, was skipped, or failed + pub status: CustomizationReloadStatus, + /// Component whose reload was attempted or skipped + pub subsystem: CustomizationReloadSubsystem, +} + +/// Results of reloading discovered session customizations. Inspect outcomes for reloaded, skipped, or failed subsystems; a rejection may follow partial mutation. Changes to the model-facing prompt and tools apply on the next turn. +/// +///
+/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
+#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct CustomizationsReloadResult { + /// Errors from any component that could not be refreshed + pub errors: Vec, + /// Outcome of each component in reload order; a skipped component was not configured or loaded + pub outcomes: Vec, + /// Warnings from skill discovery + pub warnings: Vec, +} + /// A file included in the session debug bundle. /// ///
@@ -6711,7 +6782,7 @@ pub struct DiscoveredMcpServer { pub r#type: Option, } -/// Slash-prefixed command string to enqueue for FIFO processing. +/// Vision-specific limits /// ///
/// @@ -6721,15 +6792,94 @@ pub struct DiscoveredMcpServer { ///
#[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] -pub struct EnqueueCommandParams { - /// Slash-prefixed command string to enqueue, e.g. '/compact' or '/model gpt-4'. Queued FIFO with any in-flight items; if the session is idle, processing kicks off immediately. - pub command: String, - /// Optional user-facing text for the queue row. The command string is shown when omitted. +pub struct ModelCapabilitiesLimitsVision { + /// Maximum image size in bytes + #[serde(rename = "max_prompt_image_size")] + pub max_prompt_image_size: i64, + /// Maximum number of images per prompt + #[serde(rename = "max_prompt_images")] + pub max_prompt_images: i64, + /// MIME types the model accepts + #[serde(rename = "supported_media_types")] + pub supported_media_types: Vec, +} + +/// Token limits for prompts, outputs, and context window +/// +///
+/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
+#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ModelCapabilitiesLimits { + /// Maximum total context window size in tokens + #[serde( + rename = "max_context_window_tokens", + skip_serializing_if = "Option::is_none" + )] + pub max_context_window_tokens: Option, + /// Maximum number of output/completion tokens + #[serde(rename = "max_output_tokens", skip_serializing_if = "Option::is_none")] + pub max_output_tokens: Option, + /// Maximum number of prompt/input tokens + #[serde(rename = "max_prompt_tokens", skip_serializing_if = "Option::is_none")] + pub max_prompt_tokens: Option, + /// Vision-specific limits #[serde(skip_serializing_if = "Option::is_none")] - pub display_text: Option, + pub vision: Option, +} + +/// Feature flags indicating what the model supports +/// +///
+/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
+#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ModelCapabilitiesSupports { + /// Resolved Anthropic adaptive-thinking capability — unsupported / optional / required / adaptive_only. 'required' models reject thinking.type='enabled' with HTTP 400 but still accept 'disabled' (e.g. opus-4.7/4.8/5, sonnet-5); 'adaptive_only' models accept nothing but 'adaptive' (e.g. fable, mythos). + #[serde(rename = "adaptive_thinking", skip_serializing_if = "Option::is_none")] + pub adaptive_thinking: Option, + /// Whether this model supports reasoning effort configuration + #[serde(skip_serializing_if = "Option::is_none")] + pub reasoning_effort: Option, + /// Whether the model supports provider-native thinking. Independent of configurable reasoning effort; omission means unknown. + #[serde(skip_serializing_if = "Option::is_none")] + pub thinking: Option, + /// Whether this model supports canonical tool calling + #[serde(skip_serializing_if = "Option::is_none")] + pub tool_calls: Option, + /// Whether this model supports vision/image input + #[serde(skip_serializing_if = "Option::is_none")] + pub vision: Option, +} + +/// Model capabilities and limits +/// +///
+/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
+#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ModelCapabilities { + /// Token limits for prompts, outputs, and context window + #[serde(skip_serializing_if = "Option::is_none")] + pub limits: Option, + /// Feature flags indicating what the model supports + #[serde(skip_serializing_if = "Option::is_none")] + pub supports: Option, } -/// OneAuth token request supplied by a trusted host application. +/// Provider-reported model artifact metadata. /// ///
/// @@ -6739,42 +6889,158 @@ pub struct EnqueueCommandParams { ///
#[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] -pub struct EntraTokenAcquireRequest { - /// Previously rejected token that OneAuth must bypass during renewal. +pub struct ModelArtifactDetails { + /// Provider-reported model architecture. + #[serde(skip_serializing_if = "Option::is_none")] + pub architecture: Option, + /// Provider-reported model families. + #[serde(skip_serializing_if = "Option::is_none")] + pub families: Option>, + /// Primary model family. + #[serde(skip_serializing_if = "Option::is_none")] + pub family: Option, + /// Artifact format, such as `gguf`. + #[serde(skip_serializing_if = "Option::is_none")] + pub format: Option, + /// Provider-reported parameter count label. #[serde(skip_serializing_if = "Option::is_none")] - pub access_token_to_renew: Option, - /// Public client application id. - pub client_id: String, - /// Whether the broker may show interaction. - pub interaction: EntraTokenInteraction, - /// Broker redirect URI registered for the client. Required: the OneAuth broker validates a non-empty, registered redirect URI for the public client (MSAL broker registration), so this is not a browser-flow vestige and cannot be omitted. - pub redirect_uri: String, - /// Exact delegated scopes to request. - pub scopes: Vec, - /// Tenant id or tenant selector, such as common or organizations. - pub tenant_id: String, + pub parameter_size: Option, + /// Provider-reported quantization label. + #[serde(skip_serializing_if = "Option::is_none")] + pub quantization: Option, + /// Provider-reported tokenizer. + #[serde(skip_serializing_if = "Option::is_none")] + pub tokenizer: Option, } +/// Attribution for the adapter that produced a provider row. +/// +///
+/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
#[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] -pub struct EntraTokenAcquireResultOk { - /// Opaque access token. - pub access_token: String, - /// Opaque OneAuth account id, when supplied by the broker. +pub struct ModelProviderProvenance { + /// Stable opaque adapter identity from the effective catalog. Treat this as a whole identifier, not a parseable owner or kind. + pub adapter_id: String, + /// Human-readable contributor name, not the adapter display name. #[serde(skip_serializing_if = "Option::is_none")] - pub account_id: Option, - /// Expiry as milliseconds since Unix epoch, when supplied by OneAuth. + pub owner_display_name: Option, + /// Stable contributor identifier when the adapter has an owner outside the runtime. Independent of the contribution mechanism and not a routing key. + #[serde(skip_serializing_if = "Option::is_none")] + pub owner_id: Option, + /// Descriptive provider family that produced this row; not a routing key. + pub provider_kind: String, + /// Kind of component that supplied the adapter. + pub source: ModelProviderProvenanceSource, +} + +/// A non-fatal provider observation warning. +/// +///
+/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
+#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ModelProviderWarning { + /// Machine-readable warning code. + pub code: String, + /// Human-readable warning message. + pub message: String, +} + +/// A model offered for agent conversations. Missing capability metadata does not disqualify a candidate. Models known to be incompatible, such as embedding-only models, are excluded by the adapter. +/// +///
+/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
+#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct DiscoveredModel { + /// Provider-reported model capabilities. Omitted capability fields are unknown; explicit false values are preserved. + pub capabilities: ModelCapabilities, + /// Provider-reported model artifact details. + pub details: ModelArtifactDetails, + /// Provider-reported artifact digest. + #[serde(skip_serializing_if = "Option::is_none")] + pub digest: Option, + /// Provider-native model identifier. + pub id: String, + /// Provider-reported last-modified timestamp. #[serde(skip_serializing_if = "Option::is_none")] - pub expires_on_timestamp: Option, - /// OneAuth token acquisition outcome discriminator. - pub status: EntraTokenAcquireResultOkStatus, + pub modified_at: Option, + /// Provider-reported display name. + #[serde(skip_serializing_if = "Option::is_none")] + pub name: Option, + /// Attribution for the adapter that produced this model row. + pub provenance: ModelProviderProvenance, + /// Provider-reported artifact size in bytes. + #[serde(skip_serializing_if = "Option::is_none")] + pub size_bytes: Option, + /// Non-fatal warnings encountered while enriching this model. + pub warnings: Vec, +} + +/// Typed provider-operation outcome. Use the code for control flow and the optional message for display. +/// +///
+/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
+#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ModelProviderOperationOutcome { + /// Machine-readable operation outcome. + pub code: ModelProviderOperationOutcomeCode, + /// Human-readable detail for non-success outcomes. + #[serde(skip_serializing_if = "Option::is_none")] + pub message: Option, +} + +/// Models offered for agent conversations by one provider instance. Adapters exclude known-incompatible models, but retain candidates with unknown capabilities. Listing does not guarantee compatibility. +/// +///
+/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
+#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct DiscoveredModelList { + /// Provider-native models in provider order. + pub models: Vec, + /// Typed operation outcome. + pub outcome: ModelProviderOperationOutcome, } +/// Slash-prefixed command string to enqueue for FIFO processing. +/// +///
+/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
#[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] -pub struct EntraTokenAcquireResultInteractionRequired { - /// OneAuth token acquisition outcome discriminator. - pub status: EntraTokenAcquireResultInteractionRequiredStatus, +pub struct EnqueueCommandParams { + /// Slash-prefixed command string to enqueue, e.g. '/compact' or '/model gpt-4'. Queued FIFO with any in-flight items; if the session is idle, processing kicks off immediately. + pub command: String, + /// Optional user-facing text for the queue row. The command string is shown when omitted. + #[serde(skip_serializing_if = "Option::is_none")] + pub display_text: Option, } /// Hosting capabilities and session capacity advertised by an environment. @@ -14084,100 +14350,6 @@ pub struct ModelBilling { pub token_prices: Option, } -/// Vision-specific limits -/// -///
-/// -/// **Experimental.** This type is part of an experimental wire-protocol surface -/// and may change or be removed in future SDK or CLI releases. -/// -///
-#[derive(Debug, Clone, Default, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct ModelCapabilitiesLimitsVision { - /// Maximum image size in bytes - #[serde(rename = "max_prompt_image_size")] - pub max_prompt_image_size: i64, - /// Maximum number of images per prompt - #[serde(rename = "max_prompt_images")] - pub max_prompt_images: i64, - /// MIME types the model accepts - #[serde(rename = "supported_media_types")] - pub supported_media_types: Vec, -} - -/// Token limits for prompts, outputs, and context window -/// -///
-/// -/// **Experimental.** This type is part of an experimental wire-protocol surface -/// and may change or be removed in future SDK or CLI releases. -/// -///
-#[derive(Debug, Clone, Default, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct ModelCapabilitiesLimits { - /// Maximum total context window size in tokens - #[serde( - rename = "max_context_window_tokens", - skip_serializing_if = "Option::is_none" - )] - pub max_context_window_tokens: Option, - /// Maximum number of output/completion tokens - #[serde(rename = "max_output_tokens", skip_serializing_if = "Option::is_none")] - pub max_output_tokens: Option, - /// Maximum number of prompt/input tokens - #[serde(rename = "max_prompt_tokens", skip_serializing_if = "Option::is_none")] - pub max_prompt_tokens: Option, - /// Vision-specific limits - #[serde(skip_serializing_if = "Option::is_none")] - pub vision: Option, -} - -/// Feature flags indicating what the model supports -/// -///
-/// -/// **Experimental.** This type is part of an experimental wire-protocol surface -/// and may change or be removed in future SDK or CLI releases. -/// -///
-#[derive(Debug, Clone, Default, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct ModelCapabilitiesSupports { - /// Resolved Anthropic adaptive-thinking capability — unsupported / optional / required / adaptive_only. 'required' models reject thinking.type='enabled' with HTTP 400 but still accept 'disabled' (e.g. opus-4.7/4.8/5, sonnet-5); 'adaptive_only' models accept nothing but 'adaptive' (e.g. fable, mythos). - #[serde(rename = "adaptive_thinking", skip_serializing_if = "Option::is_none")] - pub adaptive_thinking: Option, - /// Whether this model supports reasoning effort configuration - #[serde(skip_serializing_if = "Option::is_none")] - pub reasoning_effort: Option, - /// Whether this model supports canonical tool calling - #[serde(skip_serializing_if = "Option::is_none")] - pub tool_calls: Option, - /// Whether this model supports vision/image input - #[serde(skip_serializing_if = "Option::is_none")] - pub vision: Option, -} - -/// Model capabilities and limits -/// -///
-/// -/// **Experimental.** This type is part of an experimental wire-protocol surface -/// and may change or be removed in future SDK or CLI releases. -/// -///
-#[derive(Debug, Clone, Default, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct ModelCapabilities { - /// Token limits for prompts, outputs, and context window - #[serde(skip_serializing_if = "Option::is_none")] - pub limits: Option, - /// Feature flags indicating what the model supports - #[serde(skip_serializing_if = "Option::is_none")] - pub supports: Option, -} - /// A service-published message about a model, carrying a stable machine-readable code alongside human-readable text. /// ///
@@ -14523,6 +14695,235 @@ pub struct ModelPickerPersistenceRequest { pub settings_context: ModelPickerSettingsContext, } +/// Adapter-declared policy that tells clients whether discovery may run automatically. +/// +///
+/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
+#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ModelProviderAutomaticDiscoveryPolicy { + /// Whether automatic discovery is allowed, limited to configured providers, or explicit-only. + pub mode: ModelProviderAutomaticDiscoveryMode, + /// Maximum network scope used by this adapter during discovery. + pub network_scope: ModelProviderDiscoveryNetworkScope, + /// True when discovery requires non-null caller input. Omission or null is rejected before adapter execution. When false, omitted or null input selects adapter defaults without schema validation. + pub requires_input: bool, + /// True when the adapter must be enabled by a trusted owner, such as a trusted extension, before automatic discovery may run. + pub requires_trust: bool, +} + +/// An operation supported by a model-provider adapter. +/// +///
+/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
+#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ModelProviderAdapterOperationDescriptor { + /// Optional self-contained JSON Schema Draft 7 for non-null discovery input. Only supported on discover. No external references are resolved. Omitted or null input selects defaults when requiresInput is false. Without a schema, the adapter validates supplied input. + #[serde(skip_serializing_if = "Option::is_none")] + pub input_schema: Option, + /// Supported operation name: `discover`, `getStatus`, or `models.list`. Unknown names and duplicate declarations are rejected. + pub name: String, +} + +/// Contributor attribution, independent of routing identity and authorization. +/// +///
+/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
+#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ModelProviderAttribution { + /// Human-readable contributor name, not the adapter display name. + #[serde(skip_serializing_if = "Option::is_none")] + pub owner_display_name: Option, + /// Stable contributor identifier. Required and nonblank for extension and custom sources; optional for built-in and configured sources. Does not grant authority. + #[serde(skip_serializing_if = "Option::is_none")] + pub owner_id: Option, + /// Kind of component that supplied the adapter. Attribution does not confer authority. + pub source: ModelProviderProvenanceSource, +} + +/// A normalized model-provider adapter in the session's effective catalog. +/// +///
+/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
+#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ModelProviderAdapterDescriptor { + /// Stable opaque identity for routing to this adapter. Unique in the effective catalog, independent of live registration generations. + pub adapter_id: String, + /// Adapter-declared policy for passive and automatic discovery. + pub automatic_discovery: ModelProviderAutomaticDiscoveryPolicy, + /// Human-readable provider name. + pub display_name: String, + /// Operations supported by this provider adapter. + pub operations: Vec, + /// Attribution for the adapter itself. + pub provenance: ModelProviderAttribution, + /// Descriptive provider family, such as `ollama`. Different adapters may have the same family; use adapterId for routing. + pub provider_kind: String, +} + +/// Normalized model-provider adapter definitions available to the session, not discovered instances. +/// +///
+/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
+#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ModelProviderAdapterCatalog { + /// Available provider adapters ordered by adapterId. + pub providers: Vec, +} + +/// A BYOK model definition referencing a named provider. +/// +///
+/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
+#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ProviderModelConfig { + /// Optional capability overrides (vision, tool_calls, reasoning, etc.). + #[serde(skip_serializing_if = "Option::is_none")] + pub capabilities: Option, + /// Provider-local model id, unique within its provider. The session-wide selection id (shown in the model list and passed to switchTo) is the provider-qualified `provider/id`. + pub id: String, + /// Maximum context window tokens for the model. + #[serde(skip_serializing_if = "Option::is_none")] + pub max_context_window_tokens: Option, + /// Maximum output tokens for the model. + #[serde(skip_serializing_if = "Option::is_none")] + pub max_output_tokens: Option, + /// Maximum prompt/input tokens for the model. + #[serde(skip_serializing_if = "Option::is_none")] + pub max_prompt_tokens: Option, + /// Provider-published model metadata, preserved verbatim as the public Model.metadata object. + #[serde(skip_serializing_if = "Option::is_none")] + pub metadata: Option>, + /// Well-known base model id used for behavior/capability/config lookup. Defaults to `id`. + #[serde(skip_serializing_if = "Option::is_none")] + pub model_id: Option, + /// Display name for model pickers. Defaults to the provider-qualified selection id (`provider/id`). + #[serde(skip_serializing_if = "Option::is_none")] + pub name: Option, + /// Name of the configured provider that serves this model. + pub provider: String, + /// System-message configuration used when the runtime builds the standard prompt for this provider-qualified model, including general-purpose subagents. It uses the same object hierarchy as session-level systemMessage configuration, except transform actions are rejected because the current callback protocol is not model-scoped. When present, it overrides the session-wide configuration on those prompt paths. Selected custom-agent and specialized-subagent prompts remain authoritative. + #[serde(skip_serializing_if = "Option::is_none")] + pub system_message: Option, + /// The model name sent to the provider API for inference. Defaults to `id`. + #[serde(skip_serializing_if = "Option::is_none")] + pub wire_model: Option, +} + +/// Azure-specific provider options. +/// +///
+/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
+#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ProviderConfigAzure { + /// API version. When set, uses the versioned deployment route. When omitted, uses the GA versionless v1 route. + #[serde(skip_serializing_if = "Option::is_none")] + pub api_version: Option, +} + +/// External SDK input for a named custom model provider. Ingested by the native protocol boundary before host dispatch. +/// +///
+/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
+#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct NamedProviderConfig { + /// Static API key used to authenticate provider requests. + #[serde(skip_serializing_if = "Option::is_none")] + pub api_key: Option, + /// Azure authentication configuration for the provider. + #[serde(skip_serializing_if = "Option::is_none")] + pub azure: Option, + /// Base URL for provider API requests. + pub base_url: String, + /// Static bearer token used to authenticate provider requests. + #[serde(skip_serializing_if = "Option::is_none")] + pub bearer_token: Option, + /// Whether the host supplies bearer tokens dynamically. + #[serde(skip_serializing_if = "Option::is_none")] + pub has_bearer_token_provider: Option, + /// Additional HTTP headers included with provider requests. + #[serde(skip_serializing_if = "Option::is_none")] + pub headers: Option>, + /// The product serving the provider's models, reported in telemetry as `model_provider`. Only affects telemetry. + #[serde(skip_serializing_if = "Option::is_none")] + pub model_provider: Option, + /// Unique provider name used to qualify model selection IDs. + pub name: String, + /// Transport used to communicate with the provider. + #[serde(skip_serializing_if = "Option::is_none")] + pub transport: Option, + /// Provider protocol family. + #[serde(skip_serializing_if = "Option::is_none")] + pub r#type: Option, + /// Wire API used to communicate with the provider. + #[serde(skip_serializing_if = "Option::is_none")] + pub wire_api: Option, +} + +/// Provider configuration prepared from a discovered model. Preparing a plan changes nothing: it neither registers the model with the session nor writes durable configuration. To apply it, pass `provider` and `model` to `session.provider.add`, omitting whichever the dispositions report as already configured. +/// +///
+/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
+#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ModelProviderConfigurationPlan { + /// Model definition prepared from the discovered model. Capability fields the provider did not report stay omitted rather than being asserted false. + pub model: ProviderModelConfig, + /// Whether `model` still needs to be registered. When `alreadyConfigured`, `selectionId` is already registered and the caller can select it without adding anything. + pub model_disposition: ModelProviderConfigurationDisposition, + /// Provider connection prepared from the instance's inference metadata. Carries no credential; supply one if the endpoint requires it. + pub provider: NamedProviderConfig, + /// Whether `provider` still needs to be registered. When `alreadyConfigured`, a provider with the same endpoint is already registered and `provider` restates it under its existing name; adding it again is rejected as a duplicate. + pub provider_disposition: ModelProviderConfigurationDisposition, + /// Provider-qualified selection id (`provider/id`) to pass to `switchTo` once the plan is applied. + pub selection_id: String, + /// Non-fatal warnings carried over from the discovered model, such as capabilities the provider did not report. + pub warnings: Vec, +} + /// One model provider available to the session — the model analog of the account `ProviderDescriptor`. Opaque id/label/kind plus a stable ordering; central code never branches on kind. /// ///
@@ -14544,6 +14945,162 @@ pub struct ModelProviderDescriptor { pub ordering: i64, } +/// Provider discovery parameters. +/// +///
+/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
+#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ModelProviderDiscoverRequest { + /// Opaque adapter identity returned by `session.providers.getCatalog`. + pub adapter_id: String, + /// Provider-specific JSON input. Omission or null selects adapter defaults unless requiresInput is true. Non-null input is validated against the advertised Draft 7 schema when present; otherwise validation belongs to the adapter. + #[serde(skip_serializing_if = "Option::is_none")] + pub input: Option, +} + +/// Serializable reference to a discovered provider instance. +/// +///
+/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
+#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ModelProviderInstanceReference { + /// Stable opaque identity of the adapter that owns this reference. Must be present in the target session's effective catalog. + pub adapter_id: String, + /// Stable instance identifier derived by the provider adapter, such as `ollama:{normalizedEndpoint}`. + pub id: String, + /// Absolute provider management URI. The adapter validates normalization, supported schemes, and permission to access it against its bound configuration; a reference does not grant authority. + pub management_endpoint: String, + /// Descriptive provider family. Must match the selected adapter; not a routing key. + pub provider_kind: String, +} + +/// A normalized model-provider instance discovered by the runtime. +/// +///
+/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
+#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ModelProviderInstance { + /// Human-readable instance name. + pub display_name: String, + /// Inference API endpoint when the provider exposes one separately from its management endpoint. + #[serde(skip_serializing_if = "Option::is_none")] + pub inference_endpoint: Option, + /// Transport to use for inference against this instance. + #[serde(skip_serializing_if = "Option::is_none")] + pub inference_transport: Option, + /// Provider family to use for inference against this instance. + #[serde(skip_serializing_if = "Option::is_none")] + pub inference_type: Option, + /// Wire API to use for inference against this instance, when required by the provider family. + #[serde(skip_serializing_if = "Option::is_none")] + pub inference_wire_api: Option, + /// Attribution for the adapter that produced this instance. + pub provenance: ModelProviderProvenance, + /// Self-contained reference for subsequent provider operations. + pub reference: ModelProviderInstanceReference, +} + +/// Provider instances found by a discovery operation. +/// +///
+/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
+#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ModelProviderDiscoverResult { + /// Discovered provider instances. Empty when passive default discovery finds no reachable provider. + pub instances: Vec, + /// Typed operation outcome. Passive discovery can return `absent` with an empty instance list. + pub outcome: ModelProviderOperationOutcome, +} + +/// Provider status request parameters. +/// +///
+/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
+#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ModelProviderGetStatusRequest { + /// Provider instance reference returned by discovery. + pub instance: ModelProviderInstanceReference, +} + +/// Provider model inventory request parameters. +/// +///
+/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
+#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ModelProviderModelsListRequest { + /// Provider instance reference returned by discovery. + pub instance: ModelProviderInstanceReference, +} + +/// A discovered instance and one of its models to translate into provider configuration. Pass back the instance and model as returned by `session.providers.discover` and `session.providers.models.list`. +/// +///
+/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
+#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ModelProviderPrepareConfigurationRequest { + /// The discovered instance that serves the model. + pub instance: ModelProviderInstance, + /// The discovered model to configure. + pub model: DiscoveredModel, +} + +/// Current health information for a provider instance. +/// +///
+/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
+#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ModelProviderStatus { + /// Normalized provider instance. + pub instance: ModelProviderInstance, + /// Typed operation outcome. + pub outcome: ModelProviderOperationOutcome, + /// Open provider status value, such as `healthy`, `unreachable`, or `notInstalled`. + pub status: String, + /// Provider-reported version. + #[serde(skip_serializing_if = "Option::is_none")] + pub version: Option, +} + /// Host-supplied exact model selection IDs to allow for this running session. CAPI IDs are intersected with repository `.github/allowed_models.txt` policy; provider-qualified IDs remain exempt from repository-only policy but are restricted by this host list. Omit or pass null to clear the host restriction; an explicit empty or disjoint list is rejected. Validation and pre-selection fallback failures preserve the previous restriction. Failures after a fallback selection commits retain the new restriction and selected model; callers should inspect current session state after such an error. /// ///
@@ -14898,66 +15455,6 @@ pub struct MoveMcpLoadingToBackgroundResult { pub moved_to_background: bool, } -/// Azure-specific provider options. -/// -///
-/// -/// **Experimental.** This type is part of an experimental wire-protocol surface -/// and may change or be removed in future SDK or CLI releases. -/// -///
-#[derive(Debug, Clone, Default, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct ProviderConfigAzure { - /// API version. When set, uses the versioned deployment route. When omitted, uses the GA versionless v1 route. - #[serde(skip_serializing_if = "Option::is_none")] - pub api_version: Option, -} - -/// External SDK input for a named custom model provider. Ingested by the native protocol boundary before host dispatch. -/// -///
-/// -/// **Experimental.** This type is part of an experimental wire-protocol surface -/// and may change or be removed in future SDK or CLI releases. -/// -///
-#[derive(Debug, Clone, Default, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct NamedProviderConfig { - /// Static API key used to authenticate provider requests. - #[serde(skip_serializing_if = "Option::is_none")] - pub api_key: Option, - /// Azure authentication configuration for the provider. - #[serde(skip_serializing_if = "Option::is_none")] - pub azure: Option, - /// Base URL for provider API requests. - pub base_url: String, - /// Static bearer token used to authenticate provider requests. - #[serde(skip_serializing_if = "Option::is_none")] - pub bearer_token: Option, - /// Whether the host supplies bearer tokens dynamically. - #[serde(skip_serializing_if = "Option::is_none")] - pub has_bearer_token_provider: Option, - /// Additional HTTP headers included with provider requests. - #[serde(skip_serializing_if = "Option::is_none")] - pub headers: Option>, - /// The product serving the provider's models, reported in telemetry as `model_provider`. Only affects telemetry. - #[serde(skip_serializing_if = "Option::is_none")] - pub model_provider: Option, - /// Unique provider name used to qualify model selection IDs. - pub name: String, - /// Transport used to communicate with the provider. - #[serde(skip_serializing_if = "Option::is_none")] - pub transport: Option, - /// Provider protocol family. - #[serde(skip_serializing_if = "Option::is_none")] - pub r#type: Option, - /// Wire API used to communicate with the provider. - #[serde(skip_serializing_if = "Option::is_none")] - pub wire_api: Option, -} - /// The session's friendly name, or null when not yet set. /// ///
@@ -17365,50 +17862,6 @@ pub struct ProtocolSystemMessageReplaceConfig { pub mode: ProtocolReplaceMode, } -/// A BYOK model definition referencing a named provider. -/// -///
-/// -/// **Experimental.** This type is part of an experimental wire-protocol surface -/// and may change or be removed in future SDK or CLI releases. -/// -///
-#[derive(Debug, Clone, Default, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct ProviderModelConfig { - /// Optional capability overrides (vision, tool_calls, reasoning, etc.). - #[serde(skip_serializing_if = "Option::is_none")] - pub capabilities: Option, - /// Provider-local model id, unique within its provider. The session-wide selection id (shown in the model list and passed to switchTo) is the provider-qualified `provider/id`. - pub id: String, - /// Maximum context window tokens for the model. - #[serde(skip_serializing_if = "Option::is_none")] - pub max_context_window_tokens: Option, - /// Maximum output tokens for the model. - #[serde(skip_serializing_if = "Option::is_none")] - pub max_output_tokens: Option, - /// Maximum prompt/input tokens for the model. - #[serde(skip_serializing_if = "Option::is_none")] - pub max_prompt_tokens: Option, - /// Provider-published model metadata, preserved verbatim as the public Model.metadata object. - #[serde(skip_serializing_if = "Option::is_none")] - pub metadata: Option>, - /// Well-known base model id used for behavior/capability/config lookup. Defaults to `id`. - #[serde(skip_serializing_if = "Option::is_none")] - pub model_id: Option, - /// Display name for model pickers. Defaults to the provider-qualified selection id (`provider/id`). - #[serde(skip_serializing_if = "Option::is_none")] - pub name: Option, - /// Name of the configured provider that serves this model. - pub provider: String, - /// System-message configuration used when the runtime builds the standard prompt for this provider-qualified model, including general-purpose subagents. It uses the same object hierarchy as session-level systemMessage configuration, except transform actions are rejected because the current callback protocol is not model-scoped. When present, it overrides the session-wide configuration on those prompt paths. Selected custom-agent and specialized-subagent prompts remain authoritative. - #[serde(skip_serializing_if = "Option::is_none")] - pub system_message: Option, - /// The model name sent to the provider API for inference. Defaults to `id`. - #[serde(skip_serializing_if = "Option::is_none")] - pub wire_model: Option, -} - /// BYOK providers and/or models to add to the session's registry at runtime. Both fields are optional; provide providers, models, or both. /// ///
@@ -19464,6 +19917,57 @@ pub struct SandboxHostSupport { pub supported: bool, } +/// Result of creating the persistent certificate authority of the sandbox credential proxy. +/// +///
+/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
+#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SandboxProxyCaCreateResult { + /// Absolute path of the public certificate of the certificate authority, in PEM format. + pub certificate_path: String, +} + +/// Identifies the credential hosts that the persistent certificate authority of the sandbox credential proxy must cover. The runtime always adds the hosts from the saved user settings. +/// +///
+/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
+#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SandboxProxyCaRequest { + /// The sandbox configuration that the host gives its sessions. The runtime reads the credential hosts from `auth` and `credentials`; it ignores `enabled` and the other fields. + #[serde(skip_serializing_if = "Option::is_none")] + pub sandbox_config: Option, +} + +/// Status of the persistent certificate authority of the sandbox credential proxy. +/// +///
+/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
+#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SandboxProxyCaStatus { + /// Whether this process can add the certificate authority to OS trust without credentials from a different user. False where OS trust is unsupported, and on Windows when the process cannot elevate itself to write the machine trust store. When false, do not offer to set up the certificate authority. + pub can_install: bool, + /// Human-readable reason for the state. On `installed` or `notInstalled`, present only when the certificate authority must be rotated, and then says why. + #[serde(skip_serializing_if = "Option::is_none")] + pub detail: Option, + /// The state of the certificate authority. + pub state: SandboxProxyCaState, +} + /// Register an absolute-time scheduled prompt. /// ///
@@ -20299,6 +20803,9 @@ pub struct SessionFsError { /// Free-form detail about the error, for logging/diagnostics #[serde(skip_serializing_if = "Option::is_none")] pub message: Option, + /// For failed writeFile requests only: true if the provider changed the target before failing. Omit when unknown or unchanged. + #[serde(skip_serializing_if = "Option::is_none")] + pub write_changed: Option, } /// Path to test for existence in the client-provided session filesystem. @@ -20443,6 +20950,41 @@ pub struct SessionFsReaddirWithTypesResult { pub error: Option, } +/// Path of the binary file to read from the client-provided session filesystem. +/// +///
+/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
+#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SessionFsReadFileBytesRequest { + /// Target session identifier + pub session_id: SessionId, + /// Path using SessionFs conventions + pub path: String, +} + +/// File bytes as standard base64, or a filesystem error if the read failed. +/// +///
+/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
+#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SessionFsReadFileBytesResult { + /// Exact file bytes encoded as standard base64 + pub content: String, + /// Describes a filesystem error. + #[serde(skip_serializing_if = "Option::is_none")] + pub error: Option, +} + /// Path of the file to read from the client-provided session filesystem. /// ///
@@ -20531,6 +21073,9 @@ pub struct SessionFsRmRequest { #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct SessionFsSetProviderCapabilities { + /// Whether the provider supports binary reads and writes through sessionFs.readFileBytes and sessionFs.writeFileBytes + #[serde(skip_serializing_if = "Option::is_none")] + pub binary: Option, /// Whether the provider supports SQLite query/exists operations #[serde(skip_serializing_if = "Option::is_none")] pub sqlite: Option, @@ -20750,6 +21295,28 @@ pub struct SessionFsStatResult { pub size: i64, } +/// File path, standard-base64-encoded bytes to write, and optional mode for the client-provided session filesystem. +/// +///
+/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
+#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SessionFsWriteFileBytesRequest { + /// Target session identifier + pub session_id: SessionId, + /// Path using SessionFs conventions + pub path: String, + /// Exact file bytes encoded as standard base64 + pub content: String, + /// Optional POSIX-style mode for newly created files + #[serde(skip_serializing_if = "Option::is_none")] + pub mode: Option, +} + /// File path, content to write, and optional mode for the client-provided session filesystem. /// ///
@@ -28241,6 +28808,86 @@ pub struct SandboxGetHostSupportResult { pub supported: bool, } +/// Status of the persistent certificate authority of the sandbox credential proxy. +/// +///
+/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
+#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SandboxProxyCaGetStatusResult { + /// Whether this process can add the certificate authority to OS trust without credentials from a different user. False where OS trust is unsupported, and on Windows when the process cannot elevate itself to write the machine trust store. When false, do not offer to set up the certificate authority. + pub can_install: bool, + /// Human-readable reason for the state. On `installed` or `notInstalled`, present only when the certificate authority must be rotated, and then says why. + #[serde(skip_serializing_if = "Option::is_none")] + pub detail: Option, + /// The state of the certificate authority. + pub state: SandboxProxyCaState, +} + +/// Status of the persistent certificate authority of the sandbox credential proxy. +/// +///
+/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
+#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SandboxProxyCaRotateResult { + /// Whether this process can add the certificate authority to OS trust without credentials from a different user. False where OS trust is unsupported, and on Windows when the process cannot elevate itself to write the machine trust store. When false, do not offer to set up the certificate authority. + pub can_install: bool, + /// Human-readable reason for the state. On `installed` or `notInstalled`, present only when the certificate authority must be rotated, and then says why. + #[serde(skip_serializing_if = "Option::is_none")] + pub detail: Option, + /// The state of the certificate authority. + pub state: SandboxProxyCaState, +} + +/// Status of the persistent certificate authority of the sandbox credential proxy. +/// +///
+/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
+#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SandboxProxyCaTrustResult { + /// Whether this process can add the certificate authority to OS trust without credentials from a different user. False where OS trust is unsupported, and on Windows when the process cannot elevate itself to write the machine trust store. When false, do not offer to set up the certificate authority. + pub can_install: bool, + /// Human-readable reason for the state. On `installed` or `notInstalled`, present only when the certificate authority must be rotated, and then says why. + #[serde(skip_serializing_if = "Option::is_none")] + pub detail: Option, + /// The state of the certificate authority. + pub state: SandboxProxyCaState, +} + +/// Status of the persistent certificate authority of the sandbox credential proxy. +/// +///
+/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
+#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SandboxProxyCaRemoveResult { + /// Whether this process can add the certificate authority to OS trust without credentials from a different user. False where OS trust is unsupported, and on Windows when the process cannot elevate itself to write the machine trust store. When false, do not offer to set up the certificate authority. + pub can_install: bool, + /// Human-readable reason for the state. On `installed` or `notInstalled`, present only when the certificate authority must be rotated, and then says why. + #[serde(skip_serializing_if = "Option::is_none")] + pub detail: Option, + /// The state of the certificate authority. + pub state: SandboxProxyCaState, +} + /// Built-in tools available for the requested model, with their parameters and instructions. /// ///
@@ -28818,6 +29465,117 @@ pub struct SessionsGetRemoteControlStatusResult { pub status: serde_json::Value, } +/// Identifies the target session. +/// +///
+/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
+#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SessionProvidersGetCatalogParams { + /// Target session identifier + pub session_id: SessionId, +} + +/// Normalized model-provider adapter definitions available to the session, not discovered instances. +/// +///
+/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
+#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SessionProvidersGetCatalogResult { + /// Available provider adapters ordered by adapterId. + pub providers: Vec, +} + +/// Provider instances found by a discovery operation. +/// +///
+/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
+#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SessionProvidersDiscoverResult { + /// Discovered provider instances. Empty when passive default discovery finds no reachable provider. + pub instances: Vec, + /// Typed operation outcome. Passive discovery can return `absent` with an empty instance list. + pub outcome: ModelProviderOperationOutcome, +} + +/// Current health information for a provider instance. +/// +///
+/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
+#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SessionProvidersGetStatusResult { + /// Normalized provider instance. + pub instance: ModelProviderInstance, + /// Typed operation outcome. + pub outcome: ModelProviderOperationOutcome, + /// Open provider status value, such as `healthy`, `unreachable`, or `notInstalled`. + pub status: String, + /// Provider-reported version. + #[serde(skip_serializing_if = "Option::is_none")] + pub version: Option, +} + +/// Models offered for agent conversations by one provider instance. Adapters exclude known-incompatible models, but retain candidates with unknown capabilities. Listing does not guarantee compatibility. +/// +///
+/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
+#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SessionProvidersModelsListResult { + /// Provider-native models in provider order. + pub models: Vec, + /// Typed operation outcome. + pub outcome: ModelProviderOperationOutcome, +} + +/// Provider configuration prepared from a discovered model. Preparing a plan changes nothing: it neither registers the model with the session nor writes durable configuration. To apply it, pass `provider` and `model` to `session.provider.add`, omitting whichever the dispositions report as already configured. +/// +///
+/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
+#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SessionProvidersModelsPrepareConfigurationResult { + /// Model definition prepared from the discovered model. Capability fields the provider did not report stay omitted rather than being asserted false. + pub model: ProviderModelConfig, + /// Whether `model` still needs to be registered. When `alreadyConfigured`, `selectionId` is already registered and the caller can select it without adding anything. + pub model_disposition: ModelProviderConfigurationDisposition, + /// Provider connection prepared from the instance's inference metadata. Carries no credential; supply one if the endpoint requires it. + pub provider: NamedProviderConfig, + /// Whether `provider` still needs to be registered. When `alreadyConfigured`, a provider with the same endpoint is already registered and `provider` restates it under its existing name; adding it again is rejected as a duplicate. + pub provider_disposition: ModelProviderConfigurationDisposition, + /// Provider-qualified selection id (`provider/id`) to pass to `switchTo` once the plan is applied. + pub selection_id: String, + /// Non-fatal warnings carried over from the discovered model, such as capabilities the provider did not report. + pub warnings: Vec, +} + /// Identifies the target session. /// ///
@@ -29734,6 +30492,9 @@ pub struct SessionModelGetCurrentResult { /// Latest unclaimed Auto preference waiting for a future user turn. Null means the pending request is returning to provider-default routing. #[serde(skip_serializing_if = "Option::is_none")] pub pending_auto_tier: Option, + /// Captured base model to restore when leaving plan mode. Omitted outside plan mode or when no plan override has captured a base model. Persistent agent model requirements apply to this model rather than the temporary plan model. + #[serde(skip_serializing_if = "Option::is_none")] + pub plan_base_model_id: Option, /// Reasoning effort level currently applied to the active model, when one is set. Reads `Session.getReasoningEffort()` synchronously after `getSelectedModel()` resolves so the two values are reported as a snapshot. #[serde(skip_serializing_if = "Option::is_none")] pub reasoning_effort: Option, @@ -30869,23 +31630,6 @@ pub struct SessionCustomizationsReloadParams { pub session_id: SessionId, } -/// Diagnostics from reloading skill definitions, with warnings and errors as separate lists. -/// -///
-/// -/// **Experimental.** This type is part of an experimental wire-protocol surface -/// and may change or be removed in future SDK or CLI releases. -/// -///
-#[derive(Debug, Clone, Default, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct SessionCustomizationsReloadResult { - /// Errors emitted while loading skills (e.g. skills that failed to load entirely) - pub errors: Vec, - /// Warnings emitted while loading skills (e.g. skills that loaded but had issues) - pub warnings: Vec, -} - /// Indicates whether fleet mode was successfully activated. /// ///
@@ -32944,6 +33688,21 @@ pub struct SessionUiHandlePendingElicitationResult { pub success: bool, } +/// Indicates whether the elicitation response was accepted; false if it was already resolved by another client. +/// +///
+/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
+#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SessionUiHandleHumanAskUserResult { + /// Whether the response was accepted. False if the request was already resolved by another client. + pub success: bool, +} + /// Indicates whether the pending UI request was resolved by this call. /// ///
@@ -32959,6 +33718,21 @@ pub struct SessionUiHandlePendingUserInputResult { pub success: bool, } +/// Indicates whether the pending UI request was resolved by this call. +/// +///
+/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
+#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SessionUiHandleHumanUserInputResult { + /// True if the request was still pending and was resolved by this call. False if the request ID was unknown, already resolved by another client (e.g. GitHub), expired, or otherwise no longer pending. + pub success: bool, +} + /// Indicates whether the pending UI request was resolved by this call. /// ///
@@ -33019,6 +33793,21 @@ pub struct SessionUiHandlePendingExitPlanModeResult { pub success: bool, } +/// Indicates whether the pending UI request was resolved by this call. +/// +///
+/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
+#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SessionUiHandleHumanExitPlanModeResult { + /// True if the request was still pending and was resolved by this call. False if the request ID was unknown, already resolved by another client (e.g. GitHub), expired, or otherwise no longer pending. + pub success: bool, +} + /// Identifies the target session. /// ///
@@ -37913,6 +38702,124 @@ pub enum ContentFilterMode { Unknown, } +/// Result of reloading a customization component. +/// +///
+/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
+#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(from = "String", into = "String")] +pub enum CustomizationReloadStatus { + /// The component was refreshed successfully. + Reloaded, + /// The component was not configured, loaded, or eligible for refresh. + Skipped, + /// The component could not be refreshed; other components may still reload. + Failed, + /// Unknown variant for forward compatibility. + Unknown(String), +} + +impl Default for CustomizationReloadStatus { + fn default() -> Self { + Self::Unknown("unknown".to_owned()) + } +} + +impl From for CustomizationReloadStatus { + fn from(value: String) -> Self { + match value.as_str() { + "reloaded" => Self::Reloaded, + "skipped" => Self::Skipped, + "failed" => Self::Failed, + _ => Self::Unknown(value), + } + } +} + +impl From for String { + fn from(value: CustomizationReloadStatus) -> Self { + match value { + CustomizationReloadStatus::Reloaded => "reloaded".to_owned(), + CustomizationReloadStatus::Skipped => "skipped".to_owned(), + CustomizationReloadStatus::Failed => "failed".to_owned(), + CustomizationReloadStatus::Unknown(value) => value, + } + } +} + +/// Component of session customization discovery. +/// +///
+/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
+#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(from = "String", into = "String")] +pub enum CustomizationReloadSubsystem { + /// Repository metadata and working-directory context. + RepositoryContext, + /// Session instructions and their cached dynamic context. + Instructions, + /// Discovered plugin configuration. + Plugins, + /// Configured session and plugin hooks. + Hooks, + /// Discovered skills. + Skills, + /// Discovered custom agents. + Agents, + /// Loaded MCP server configuration. + Mcp, + /// Configured session extensions. + Extensions, + /// Unknown variant for forward compatibility. + Unknown(String), +} + +impl Default for CustomizationReloadSubsystem { + fn default() -> Self { + Self::Unknown("unknown".to_owned()) + } +} + +impl From for CustomizationReloadSubsystem { + fn from(value: String) -> Self { + match value.as_str() { + "repositoryContext" => Self::RepositoryContext, + "instructions" => Self::Instructions, + "plugins" => Self::Plugins, + "hooks" => Self::Hooks, + "skills" => Self::Skills, + "agents" => Self::Agents, + "mcp" => Self::Mcp, + "extensions" => Self::Extensions, + _ => Self::Unknown(value), + } + } +} + +impl From for String { + fn from(value: CustomizationReloadSubsystem) -> Self { + match value { + CustomizationReloadSubsystem::RepositoryContext => "repositoryContext".to_owned(), + CustomizationReloadSubsystem::Instructions => "instructions".to_owned(), + CustomizationReloadSubsystem::Plugins => "plugins".to_owned(), + CustomizationReloadSubsystem::Hooks => "hooks".to_owned(), + CustomizationReloadSubsystem::Skills => "skills".to_owned(), + CustomizationReloadSubsystem::Agents => "agents".to_owned(), + CustomizationReloadSubsystem::Mcp => "mcp".to_owned(), + CustomizationReloadSubsystem::Extensions => "extensions".to_owned(), + CustomizationReloadSubsystem::Unknown(value) => value, + } + } +} + /// Source category for a collected debug bundle entry. /// ///
@@ -38382,7 +39289,7 @@ pub enum DiscoveredMcpServerType { Unknown, } -/// How far OneAuth may go to acquire the requested token. +/// Kind of component that supplied a provider adapter or row. Attribution does not confer authority. /// ///
/// @@ -38391,39 +39298,26 @@ pub enum DiscoveredMcpServerType { /// ///
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] -pub enum EntraTokenInteraction { - /// Acquire the token without any user interaction, failing if interaction would be required. - #[serde(rename = "silent")] - Silent, - /// Allow interactive acquisition, prompting the user only when a cached or silent token is unavailable. - #[serde(rename = "interactive")] - Interactive, - /// Always prompt interactively, bypassing any cached or silently-refreshable token. - #[serde(rename = "force-interactive")] - ForceInteractive, +pub enum ModelProviderProvenanceSource { + /// Built into the runtime. + #[serde(rename = "builtIn")] + BuiltIn, + /// Derived from existing user configuration. + #[serde(rename = "configured")] + Configured, + /// Supplied by an extension. + #[serde(rename = "extension")] + Extension, + /// Supplied by another trusted contributor. + #[serde(rename = "custom")] + Custom, /// Unknown variant for forward compatibility. #[default] #[serde(other)] Unknown, } -/// OneAuth token acquisition outcome discriminator. -#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] -pub enum EntraTokenAcquireResultOkStatus { - #[serde(rename = "ok")] - #[default] - Ok, -} - -/// OneAuth token acquisition outcome discriminator. -#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] -pub enum EntraTokenAcquireResultInteractionRequiredStatus { - #[serde(rename = "interaction-required")] - #[default] - InteractionRequired, -} - -/// Result of a OneAuth token acquisition. +/// Typed outcome for a provider operation. /// ///
/// @@ -38431,11 +39325,24 @@ pub enum EntraTokenAcquireResultInteractionRequiredStatus { /// and may change or be removed in future SDK or CLI releases. /// ///
-#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(untagged)] -pub enum EntraTokenAcquireResult { - Ok(EntraTokenAcquireResultOk), - InteractionRequired(EntraTokenAcquireResultInteractionRequired), +#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] +pub enum ModelProviderOperationOutcomeCode { + /// The operation completed successfully; an empty inventory is valid. + #[serde(rename = "success")] + Success, + /// The provider or instance is absent during discovery, status, or model listing. Distinct from a successful empty inventory. + #[serde(rename = "absent")] + Absent, + /// The provider is configured or expected but could not be reached. + #[serde(rename = "unreachable")] + Unreachable, + /// The operation failed for a reason other than absence or reachability. + #[serde(rename = "failed")] + Failed, + /// Unknown variant for forward compatibility. + #[default] + #[serde(other)] + Unknown, } /// GitHub Mission Control compute kind. @@ -40942,7 +41849,7 @@ pub enum ModelProviderKind { Unknown, } -/// Whether the requested preference was already effective or was accepted for later transactional activation. +/// When the runtime may run an adapter without an explicit user action. /// ///
/// @@ -40951,13 +41858,69 @@ pub enum ModelProviderKind { /// ///
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] -pub enum ModelSwitchAutoTierStatus { - /// The requested preference is already effective. No activation is pending for it, although this request may have cancelled an earlier unclaimed preference reported in `supersededAutoTier`. - #[serde(rename = "unchanged")] - Unchanged, - /// The request was accepted but has not committed. A later user turn using the `auto` model must mint and validate the replacement before it becomes effective. - #[serde(rename = "pending")] - Pending, +pub enum ModelProviderAutomaticDiscoveryMode { + /// The adapter declares that automatic discovery is safe when the other policy fields are satisfied. + #[serde(rename = "automatic")] + Automatic, + /// The adapter may refresh instances the user already configured, but must not scan for new instances automatically. + #[serde(rename = "configuredOnly")] + ConfiguredOnly, + /// The adapter must run only after an explicit user action. + #[serde(rename = "explicit")] + Explicit, + /// Unknown variant for forward compatibility. + #[default] + #[serde(other)] + Unknown, +} + +/// Network reach an adapter may use during discovery. +/// +///
+/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
+#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] +pub enum ModelProviderDiscoveryNetworkScope { + /// Discovery does not contact a network service. + #[serde(rename = "none")] + None, + /// Discovery is limited to loopback addresses on the local machine. + #[serde(rename = "loopbackOnly")] + LoopbackOnly, + /// Discovery contacts only endpoints the user already configured. + #[serde(rename = "configuredEndpointOnly")] + ConfiguredEndpointOnly, + /// Discovery may scan or contact the local network. + #[serde(rename = "localNetwork")] + LocalNetwork, + /// Discovery may contact remote internet services. + #[serde(rename = "internet")] + Internet, + /// Unknown variant for forward compatibility. + #[default] + #[serde(other)] + Unknown, +} + +/// Whether a planned configuration entry is new or already present in the session registry. +/// +///
+/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
+#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] +pub enum ModelProviderConfigurationDisposition { + /// No matching entry is registered; the caller should add the entry. + #[serde(rename = "create")] + Create, + /// An equivalent entry is already registered; the caller should reuse it rather than adding a duplicate. + #[serde(rename = "alreadyConfigured")] + AlreadyConfigured, /// Unknown variant for forward compatibility. #[default] #[serde(other)] @@ -41070,6 +42033,97 @@ pub enum ProviderConfigWireApi { Unknown, } +/// Transport to be used for provider requests. +/// +///
+/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
+#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] +pub enum ProviderEndpointTransport { + /// HTTP request/streaming transport. + #[serde(rename = "http")] + Http, + /// WebSocket transport. + #[serde(rename = "websockets")] + Websockets, + /// Unknown variant for forward compatibility. + #[default] + #[serde(other)] + Unknown, +} + +/// Provider family. Matches the `type` field of a BYOK provider config. +/// +///
+/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
+#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] +pub enum ProviderEndpointType { + /// OpenAI-compatible endpoint (use the OpenAI client library). + #[serde(rename = "openai")] + Openai, + /// Azure OpenAI endpoint (use the OpenAI client library with the Azure base URL). + #[serde(rename = "azure")] + Azure, + /// Anthropic endpoint (use the Anthropic client library). + #[serde(rename = "anthropic")] + Anthropic, + /// Unknown variant for forward compatibility. + #[default] + #[serde(other)] + Unknown, +} + +/// Wire API to be used, when required for the provider type. +/// +///
+/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
+#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] +pub enum ProviderEndpointWireApi { + /// Classic chat-completions request shape. + #[serde(rename = "completions")] + Completions, + /// Newer responses request shape. + #[serde(rename = "responses")] + Responses, + /// Unknown variant for forward compatibility. + #[default] + #[serde(other)] + Unknown, +} + +/// Whether the requested preference was already effective or was accepted for later transactional activation. +/// +///
+/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
+#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] +pub enum ModelSwitchAutoTierStatus { + /// The requested preference is already effective. No activation is pending for it, although this request may have cancelled an earlier unclaimed preference reported in `supersededAutoTier`. + #[serde(rename = "unchanged")] + Unchanged, + /// The request was accepted but has not committed. A later user turn using the `auto` model must mint and validate the replacement before it becomes effective. + #[serde(rename = "pending")] + Pending, + /// Unknown variant for forward compatibility. + #[default] + #[serde(other)] + Unknown, +} + /// Allowed values for the `OptionsUpdateAdditionalContentExclusionPolicyScope` enumeration. /// ///
@@ -42014,75 +43068,6 @@ pub enum ProtocolStaticSectionAction { Unknown, } -/// Transport to be used for provider requests. -/// -///
-/// -/// **Experimental.** This type is part of an experimental wire-protocol surface -/// and may change or be removed in future SDK or CLI releases. -/// -///
-#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] -pub enum ProviderEndpointTransport { - /// HTTP request/streaming transport. - #[serde(rename = "http")] - Http, - /// WebSocket transport. - #[serde(rename = "websockets")] - Websockets, - /// Unknown variant for forward compatibility. - #[default] - #[serde(other)] - Unknown, -} - -/// Provider family. Matches the `type` field of a BYOK provider config. -/// -///
-/// -/// **Experimental.** This type is part of an experimental wire-protocol surface -/// and may change or be removed in future SDK or CLI releases. -/// -///
-#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] -pub enum ProviderEndpointType { - /// OpenAI-compatible endpoint (use the OpenAI client library). - #[serde(rename = "openai")] - Openai, - /// Azure OpenAI endpoint (use the OpenAI client library with the Azure base URL). - #[serde(rename = "azure")] - Azure, - /// Anthropic endpoint (use the Anthropic client library). - #[serde(rename = "anthropic")] - Anthropic, - /// Unknown variant for forward compatibility. - #[default] - #[serde(other)] - Unknown, -} - -/// Wire API to be used, when required for the provider type. -/// -///
-/// -/// **Experimental.** This type is part of an experimental wire-protocol surface -/// and may change or be removed in future SDK or CLI releases. -/// -///
-#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] -pub enum ProviderEndpointWireApi { - /// Classic chat-completions request shape. - #[serde(rename = "completions")] - Completions, - /// Newer responses request shape. - #[serde(rename = "responses")] - Responses, - /// Unknown variant for forward compatibility. - #[default] - #[serde(other)] - Unknown, -} - /// Attachment type discriminator #[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] pub enum PushAttachmentBlobType { @@ -42436,6 +43421,34 @@ pub enum SandboxConfigSource { Unknown, } +/// State of the persistent certificate authority of the sandbox credential proxy. +/// +///
+/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
+#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] +pub enum SandboxProxyCaState { + /// This platform has no supported OS trust store. The proxy uses a per-process certificate bundle. + #[serde(rename = "unsupported")] + Unsupported, + /// OS trust does not include the certificate authority, or none is stored. + #[serde(rename = "notInstalled")] + NotInstalled, + /// OS trust includes the stored certificate authority. + #[serde(rename = "installed")] + Installed, + /// The runtime could not read the certificate authority or the OS trust store. + #[serde(rename = "error")] + Error, + /// Unknown variant for forward compatibility. + #[default] + #[serde(other)] + Unknown, +} + /// A session-scoped sandbox transition applied while handling a slash command /// ///
diff --git a/rust/src/generated/rpc.rs b/rust/src/generated/rpc.rs index cf87a42bd1..cf5da5b4a5 100644 --- a/rust/src/generated/rpc.rs +++ b/rust/src/generated/rpc.rs @@ -29,13 +29,6 @@ impl<'a> ClientRpc<'a> { } } - /// `accounts.*` sub-namespace. - pub fn accounts(&self) -> ClientRpcAccounts<'a> { - ClientRpcAccounts { - client: self.client, - } - } - /// `agentRegistry.*` sub-namespace. pub fn agent_registry(&self) -> ClientRpcAgentRegistry<'a> { ClientRpcAgentRegistry { @@ -437,45 +430,6 @@ impl<'a> ClientRpcAccount<'a> { } } -/// `accounts.*` RPCs. -#[derive(Clone, Copy)] -pub struct ClientRpcAccounts<'a> { - pub(crate) client: &'a Client, -} - -impl<'a> ClientRpcAccounts<'a> { - /// Acquire a Microsoft Entra access token through the runtime's OneAuth broker. Account-scoped because it uses the same native broker as the account stack: a trusted host application mints a scoped Entra token for its own use, most notably to authenticate to a remote MCP server whose authorization server is Entra ID (in place of the generic browser-OAuth flow). - /// - /// Wire method: `accounts.acquireEntraToken`. - /// - /// # Parameters - /// - /// * `params` - OneAuth token request supplied by a trusted host application. - /// - /// # Returns - /// - /// Result of a OneAuth token acquisition. - /// - ///
- /// - /// **Experimental.** This API is part of an experimental wire-protocol surface - /// and may change or be removed in future SDK or CLI releases. Pin both the - /// SDK and CLI versions if your code depends on it. - /// - ///
- pub(crate) async fn acquire_entra_token( - &self, - params: EntraTokenAcquireRequest, - ) -> Result { - let wire_params = serde_json::to_value(params)?; - let _value = self - .client - .call(rpc_methods::ACCOUNTS_ACQUIREENTRATOKEN, Some(wire_params)) - .await?; - Ok(serde_json::from_value(_value)?) - } -} - /// `agentRegistry.*` RPCs. #[derive(Clone, Copy)] pub struct ClientRpcAgentRegistry<'a> { @@ -2612,6 +2566,13 @@ pub struct ClientRpcSandbox<'a> { } impl<'a> ClientRpcSandbox<'a> { + /// `sandbox.proxyCa.*` sub-namespace. + pub fn proxy_ca(&self) -> ClientRpcSandboxProxyCa<'a> { + ClientRpcSandboxProxyCa { + client: self.client, + } + } + /// Reports whether the host running this runtime can run the command sandbox, without starting a session or spawning a sandboxed command. /// /// Wire method: `sandbox.getHostSupport`. @@ -2637,6 +2598,162 @@ impl<'a> ClientRpcSandbox<'a> { } } +/// `sandbox.proxyCa.*` RPCs. +#[derive(Clone, Copy)] +pub struct ClientRpcSandboxProxyCa<'a> { + pub(crate) client: &'a Client, +} + +impl<'a> ClientRpcSandboxProxyCa<'a> { + /// Reports whether the persistent certificate authority of the sandbox credential proxy exists, whether OS trust includes it, and whether it must be rotated. Changes nothing. + /// + /// Wire method: `sandbox.proxyCa.getStatus`. + /// + /// # Parameters + /// + /// * `params` - Identifies the credential hosts that the persistent certificate authority of the sandbox credential proxy must cover. The runtime always adds the hosts from the saved user settings. + /// + /// # Returns + /// + /// Status of the persistent certificate authority of the sandbox credential proxy. + /// + ///
+ /// + /// **Experimental.** This API is part of an experimental wire-protocol surface + /// and may change or be removed in future SDK or CLI releases. Pin both the + /// SDK and CLI versions if your code depends on it. + /// + ///
+ pub async fn get_status( + &self, + params: SandboxProxyCaRequest, + ) -> Result { + let wire_params = serde_json::to_value(params)?; + let _value = self + .client + .call(rpc_methods::SANDBOX_PROXYCA_GETSTATUS, Some(wire_params)) + .await?; + Ok(serde_json::from_value(_value)?) + } + + /// Creates the persistent certificate authority of the sandbox credential proxy if none is stored, without changing OS trust, and returns the path of its public certificate. Keeps an existing certificate authority, even one that must be rotated. Fails where OS trust is unsupported. Trust it with sandbox.proxyCa.trust: the CLI trusts only the hosts in the saved user settings, so it refuses a certificate authority that also covers hosts from sandboxConfig. + /// + /// Wire method: `sandbox.proxyCa.create`. + /// + /// # Parameters + /// + /// * `params` - Identifies the credential hosts that the persistent certificate authority of the sandbox credential proxy must cover. The runtime always adds the hosts from the saved user settings. + /// + /// # Returns + /// + /// Result of creating the persistent certificate authority of the sandbox credential proxy. + /// + ///
+ /// + /// **Experimental.** This API is part of an experimental wire-protocol surface + /// and may change or be removed in future SDK or CLI releases. Pin both the + /// SDK and CLI versions if your code depends on it. + /// + ///
+ pub async fn create( + &self, + params: SandboxProxyCaRequest, + ) -> Result { + let wire_params = serde_json::to_value(params)?; + let _value = self + .client + .call(rpc_methods::SANDBOX_PROXYCA_CREATE, Some(wire_params)) + .await?; + Ok(serde_json::from_value(_value)?) + } + + /// Replaces the persistent certificate authority of the sandbox credential proxy with a new one for the current credential hosts. If OS trust included the old one, removes it and trusts the new one, which can show an OS authentication prompt. Running sandboxed tools keep the old certificate authority until they restart. + /// + /// Wire method: `sandbox.proxyCa.rotate`. + /// + /// # Parameters + /// + /// * `params` - Identifies the credential hosts that the persistent certificate authority of the sandbox credential proxy must cover. The runtime always adds the hosts from the saved user settings. + /// + /// # Returns + /// + /// Status of the persistent certificate authority of the sandbox credential proxy. + /// + ///
+ /// + /// **Experimental.** This API is part of an experimental wire-protocol surface + /// and may change or be removed in future SDK or CLI releases. Pin both the + /// SDK and CLI versions if your code depends on it. + /// + ///
+ pub async fn rotate( + &self, + params: SandboxProxyCaRequest, + ) -> Result { + let wire_params = serde_json::to_value(params)?; + let _value = self + .client + .call(rpc_methods::SANDBOX_PROXYCA_ROTATE, Some(wire_params)) + .await?; + Ok(serde_json::from_value(_value)?) + } + + /// Adds the persistent certificate authority of the sandbox credential proxy to OS trust, so sandboxed clients that read only OS trust accept the proxy. Call create first. Refuses a certificate authority that is not constrained to the current credential hosts. Can show an OS authentication prompt. + /// + /// Wire method: `sandbox.proxyCa.trust`. + /// + /// # Parameters + /// + /// * `params` - Identifies the credential hosts that the persistent certificate authority of the sandbox credential proxy must cover. The runtime always adds the hosts from the saved user settings. + /// + /// # Returns + /// + /// Status of the persistent certificate authority of the sandbox credential proxy. + /// + ///
+ /// + /// **Experimental.** This API is part of an experimental wire-protocol surface + /// and may change or be removed in future SDK or CLI releases. Pin both the + /// SDK and CLI versions if your code depends on it. + /// + ///
+ pub async fn trust( + &self, + params: SandboxProxyCaRequest, + ) -> Result { + let wire_params = serde_json::to_value(params)?; + let _value = self + .client + .call(rpc_methods::SANDBOX_PROXYCA_TRUST, Some(wire_params)) + .await?; + Ok(serde_json::from_value(_value)?) + } + + /// Removes the persistent certificate authority of the sandbox credential proxy from OS trust. Keeps the stored certificate authority. Can show an OS authentication prompt. Sandboxed clients that read only OS trust then reject the proxy; clients that read the per-process certificate bundle continue to work. + /// + /// Wire method: `sandbox.proxyCa.remove`. + /// + /// # Returns + /// + /// Status of the persistent certificate authority of the sandbox credential proxy. + /// + ///
+ /// + /// **Experimental.** This API is part of an experimental wire-protocol surface + /// and may change or be removed in future SDK or CLI releases. Pin both the + /// SDK and CLI versions if your code depends on it. + /// + ///
+ pub async fn remove(&self) -> Result { + let wire_params = serde_json::json!({}); + let _value = self + .client + .call(rpc_methods::SANDBOX_PROXYCA_REMOVE, Some(wire_params)) + .await?; + Ok(serde_json::from_value(_value)?) + } +} + /// `secrets.*` RPCs. #[derive(Clone, Copy)] pub struct ClientRpcSecrets<'a> { @@ -4536,6 +4653,13 @@ impl<'a> SessionRpc<'a> { } } + /// `session.providers.*` sub-namespace. + pub fn providers(&self) -> SessionRpcProviders<'a> { + SessionRpcProviders { + session: self.session, + } + } + /// `session.queue.*` sub-namespace. pub fn queue(&self) -> SessionRpcQueue<'a> { SessionRpcQueue { @@ -6402,13 +6526,13 @@ pub struct SessionRpcCustomizations<'a> { } impl<'a> SessionRpcCustomizations<'a> { - /// Reloads all repository and user customizations for the active session: instructions, plugins and their MCP servers and hooks, custom agents, extensions, and skills. Returns diagnostics from the final skill reload. + /// For local sessions, reconciles repository context and discovered instructions, plugins, skills, agents, hooks, MCP servers, and extensions after files appear or change under the working directory. Independent component failures are returned in outcomes and errors; a rejected call can have partially applied earlier steps. Remote sessions must reload on their agent host instead. The model-facing context is rebuilt on the next turn. /// /// Wire method: `session.customizations.reload`. /// /// # Returns /// - /// Diagnostics from reloading skill definitions, with warnings and errors as separate lists. + /// Results of reloading discovered session customizations. Inspect outcomes for reloaded, skipped, or failed subsystems; a rejection may follow partial mutation. Changes to the model-facing prompt and tools apply on the next turn. /// ///
/// @@ -6417,7 +6541,7 @@ impl<'a> SessionRpcCustomizations<'a> { /// SDK and CLI versions if your code depends on it. /// ///
- pub async fn reload(&self) -> Result { + pub async fn reload(&self) -> Result { let wire_params = serde_json::json!({ "sessionId": self.session.id() }); let _value = self .session @@ -7515,10 +7639,14 @@ impl<'a> SessionRpcInstructions<'a> { Ok(serde_json::from_value(_value)?) } - /// Invalidates cached custom-instruction discovery so subsequent turns and source reads observe instruction files currently on disk. + /// For local sessions, invalidates instruction discovery and the model-facing prompt, then returns freshly discovered sources. The updated prompt takes effect on the next turn. Remote sessions must reload on their agent host instead. /// /// Wire method: `session.instructions.reload`. /// + /// # Returns + /// + /// Instruction sources loaded for the session, in merge order. + /// ///
/// /// **Experimental.** This API is part of an experimental wire-protocol surface @@ -7526,14 +7654,14 @@ impl<'a> SessionRpcInstructions<'a> { /// SDK and CLI versions if your code depends on it. /// ///
- pub async fn reload(&self) -> Result<(), Error> { + pub async fn reload(&self) -> Result { let wire_params = serde_json::json!({ "sessionId": self.session.id() }); let _value = self .session .client() .call(rpc_methods::SESSION_INSTRUCTIONS_RELOAD, Some(wire_params)) .await?; - Ok(()) + Ok(serde_json::from_value(_value)?) } } @@ -11496,6 +11624,192 @@ impl<'a> SessionRpcProvider<'a> { } } +/// `session.providers.*` RPCs. +#[derive(Clone, Copy)] +pub struct SessionRpcProviders<'a> { + pub(crate) session: &'a Session, +} + +impl<'a> SessionRpcProviders<'a> { + /// `session.providers.models.*` sub-namespace. + pub fn models(&self) -> SessionRpcProvidersModels<'a> { + SessionRpcProvidersModels { + session: self.session, + } + } + + /// Returns adapter definitions and supported operations in this session's effective provider catalog, without running discovery. Does not list provider instances or select inference models. + /// + /// Wire method: `session.providers.getCatalog`. + /// + /// # Returns + /// + /// Normalized model-provider adapter definitions available to the session, not discovered instances. + /// + ///
+ /// + /// **Experimental.** This API is part of an experimental wire-protocol surface + /// and may change or be removed in future SDK or CLI releases. Pin both the + /// SDK and CLI versions if your code depends on it. + /// + ///
+ pub async fn get_catalog(&self) -> Result { + let wire_params = serde_json::json!({ "sessionId": self.session.id() }); + let _value = self + .session + .client() + .call(rpc_methods::SESSION_PROVIDERS_GETCATALOG, Some(wire_params)) + .await?; + Ok(serde_json::from_value(_value)?) + } + + /// Discovers reachable instances using an adapter from this session's effective provider catalog and provider-specific discovery input. + /// + /// Wire method: `session.providers.discover`. + /// + /// # Parameters + /// + /// * `params` - Provider discovery parameters. + /// + /// # Returns + /// + /// Provider instances found by a discovery operation. + /// + ///
+ /// + /// **Experimental.** This API is part of an experimental wire-protocol surface + /// and may change or be removed in future SDK or CLI releases. Pin both the + /// SDK and CLI versions if your code depends on it. + /// + ///
+ pub async fn discover( + &self, + params: ModelProviderDiscoverRequest, + ) -> Result { + let mut wire_params = serde_json::to_value(params)?; + wire_params["sessionId"] = serde_json::Value::String(self.session.id().to_string()); + let _value = self + .session + .client() + .call(rpc_methods::SESSION_PROVIDERS_DISCOVER, Some(wire_params)) + .await?; + Ok(serde_json::from_value(_value)?) + } + + /// Gets current health and version information for a discovered model-provider instance. + /// + /// Wire method: `session.providers.getStatus`. + /// + /// # Parameters + /// + /// * `params` - Provider status request parameters. + /// + /// # Returns + /// + /// Current health information for a provider instance. + /// + ///
+ /// + /// **Experimental.** This API is part of an experimental wire-protocol surface + /// and may change or be removed in future SDK or CLI releases. Pin both the + /// SDK and CLI versions if your code depends on it. + /// + ///
+ pub async fn get_status( + &self, + params: ModelProviderGetStatusRequest, + ) -> Result { + let mut wire_params = serde_json::to_value(params)?; + wire_params["sessionId"] = serde_json::Value::String(self.session.id().to_string()); + let _value = self + .session + .client() + .call(rpc_methods::SESSION_PROVIDERS_GETSTATUS, Some(wire_params)) + .await?; + Ok(serde_json::from_value(_value)?) + } +} + +/// `session.providers.models.*` RPCs. +#[derive(Clone, Copy)] +pub struct SessionRpcProvidersModels<'a> { + pub(crate) session: &'a Session, +} + +impl<'a> SessionRpcProvidersModels<'a> { + /// Lists models installed or otherwise available from a discovered model-provider instance. + /// + /// Wire method: `session.providers.models.list`. + /// + /// # Parameters + /// + /// * `params` - Provider model inventory request parameters. + /// + /// # Returns + /// + /// Models offered for agent conversations by one provider instance. Adapters exclude known-incompatible models, but retain candidates with unknown capabilities. Listing does not guarantee compatibility. + /// + ///
+ /// + /// **Experimental.** This API is part of an experimental wire-protocol surface + /// and may change or be removed in future SDK or CLI releases. Pin both the + /// SDK and CLI versions if your code depends on it. + /// + ///
+ pub async fn list( + &self, + params: ModelProviderModelsListRequest, + ) -> Result { + let mut wire_params = serde_json::to_value(params)?; + wire_params["sessionId"] = serde_json::Value::String(self.session.id().to_string()); + let _value = self + .session + .client() + .call( + rpc_methods::SESSION_PROVIDERS_MODELS_LIST, + Some(wire_params), + ) + .await?; + Ok(serde_json::from_value(_value)?) + } + + /// Translates a discovered model into the provider and model configuration needed to use it, and reports whether each is already registered in this session. Prepares only: it registers nothing, writes nothing, and performs no provider requests. + /// + /// Wire method: `session.providers.models.prepareConfiguration`. + /// + /// # Parameters + /// + /// * `params` - A discovered instance and one of its models to translate into provider configuration. Pass back the instance and model as returned by `session.providers.discover` and `session.providers.models.list`. + /// + /// # Returns + /// + /// Provider configuration prepared from a discovered model. Preparing a plan changes nothing: it neither registers the model with the session nor writes durable configuration. To apply it, pass `provider` and `model` to `session.provider.add`, omitting whichever the dispositions report as already configured. + /// + ///
+ /// + /// **Experimental.** This API is part of an experimental wire-protocol surface + /// and may change or be removed in future SDK or CLI releases. Pin both the + /// SDK and CLI versions if your code depends on it. + /// + ///
+ pub async fn prepare_configuration( + &self, + params: ModelProviderPrepareConfigurationRequest, + ) -> Result { + let mut wire_params = serde_json::to_value(params)?; + wire_params["sessionId"] = serde_json::Value::String(self.session.id().to_string()); + let _value = self + .session + .client() + .call( + rpc_methods::SESSION_PROVIDERS_MODELS_PREPARECONFIGURATION, + Some(wire_params), + ) + .await?; + Ok(serde_json::from_value(_value)?) + } +} + /// `session.queue.*` RPCs. #[derive(Clone, Copy)] pub struct SessionRpcQueue<'a> { @@ -13801,6 +14115,42 @@ impl<'a> SessionRpcUi<'a> { Ok(serde_json::from_value(_value)?) } + /// Resolves a pending elicitation request after direct interaction in the trusted in-process client. Only an accepted response to the built-in ask_user tool can become trusted human evidence. + /// + /// Wire method: `session.ui.handleHumanAskUser`. + /// + /// # Parameters + /// + /// * `params` - Pending elicitation request ID and the user's response (accept/decline/cancel + form values). + /// + /// # Returns + /// + /// Indicates whether the elicitation response was accepted; false if it was already resolved by another client. + /// + ///
+ /// + /// **Experimental.** This API is part of an experimental wire-protocol surface + /// and may change or be removed in future SDK or CLI releases. Pin both the + /// SDK and CLI versions if your code depends on it. + /// + ///
+ pub(crate) async fn handle_human_ask_user( + &self, + params: UIHandlePendingElicitationRequest, + ) -> Result { + let mut wire_params = serde_json::to_value(params)?; + wire_params["sessionId"] = serde_json::Value::String(self.session.id().to_string()); + let _value = self + .session + .client() + .call( + rpc_methods::SESSION_UI_HANDLEHUMANASKUSER, + Some(wire_params), + ) + .await?; + Ok(serde_json::from_value(_value)?) + } + /// Resolves a pending `user_input.requested` event with the user's response. /// /// Wire method: `session.ui.handlePendingUserInput`. @@ -13837,6 +14187,42 @@ impl<'a> SessionRpcUi<'a> { Ok(serde_json::from_value(_value)?) } + /// Resolves a pending `user_input.requested` event after direct interaction in the trusted in-process client. + /// + /// Wire method: `session.ui.handleHumanUserInput`. + /// + /// # Parameters + /// + /// * `params` - Request ID of a pending `user_input.requested` event and the user's response. + /// + /// # Returns + /// + /// Indicates whether the pending UI request was resolved by this call. + /// + ///
+ /// + /// **Experimental.** This API is part of an experimental wire-protocol surface + /// and may change or be removed in future SDK or CLI releases. Pin both the + /// SDK and CLI versions if your code depends on it. + /// + ///
+ pub(crate) async fn handle_human_user_input( + &self, + params: UIHandlePendingUserInputRequest, + ) -> Result { + let mut wire_params = serde_json::to_value(params)?; + wire_params["sessionId"] = serde_json::Value::String(self.session.id().to_string()); + let _value = self + .session + .client() + .call( + rpc_methods::SESSION_UI_HANDLEHUMANUSERINPUT, + Some(wire_params), + ) + .await?; + Ok(serde_json::from_value(_value)?) + } + /// Resolves a pending `sampling.requested` event with a sampling result, or rejects it. /// /// Wire method: `session.ui.handlePendingSampling`. @@ -13981,6 +14367,42 @@ impl<'a> SessionRpcUi<'a> { Ok(serde_json::from_value(_value)?) } + /// Resolves a pending `exit_plan_mode.requested` event after direct interaction in the trusted in-process client. + /// + /// Wire method: `session.ui.handleHumanExitPlanMode`. + /// + /// # Parameters + /// + /// * `params` - Request ID of a pending `exit_plan_mode.requested` event and the user's response. + /// + /// # Returns + /// + /// Indicates whether the pending UI request was resolved by this call. + /// + ///
+ /// + /// **Experimental.** This API is part of an experimental wire-protocol surface + /// and may change or be removed in future SDK or CLI releases. Pin both the + /// SDK and CLI versions if your code depends on it. + /// + ///
+ pub(crate) async fn handle_human_exit_plan_mode( + &self, + params: UIHandlePendingExitPlanModeRequest, + ) -> Result { + let mut wire_params = serde_json::to_value(params)?; + wire_params["sessionId"] = serde_json::Value::String(self.session.id().to_string()); + let _value = self + .session + .client() + .call( + rpc_methods::SESSION_UI_HANDLEHUMANEXITPLANMODE, + Some(wire_params), + ) + .await?; + Ok(serde_json::from_value(_value)?) + } + /// Registers an in-process handler for auto-mode-switch requests so the server bridge skips dispatch. /// /// Wire method: `session.ui.registerDirectAutoModeSwitchHandler`. diff --git a/rust/src/generated/session_events.rs b/rust/src/generated/session_events.rs index 4a73d8223d..569a16a92c 100644 --- a/rust/src/generated/session_events.rs +++ b/rust/src/generated/session_events.rs @@ -228,8 +228,12 @@ pub enum SessionEventType { ToolUserRequested, #[serde(rename = "tool.execution_start")] ToolExecutionStart, + #[doc(hidden)] + #[deprecated] #[serde(rename = "tool.execution_partial_result")] ToolExecutionPartialResult, + #[serde(rename = "tool.shell_output")] + ToolShellOutput, #[serde(rename = "tool.execution_progress")] ToolExecutionProgress, #[serde(rename = "tool.execution_complete")] @@ -440,6 +444,8 @@ pub enum SessionEventType { ExitPlanModeRequested, #[serde(rename = "exit_plan_mode.completed")] ExitPlanModeCompleted, + #[serde(rename = "human_response.recorded")] + HumanResponseRecorded, #[serde(rename = "session.tools_updated")] SessionToolsUpdated, #[serde(rename = "session.background_tasks_changed")] @@ -778,8 +784,12 @@ pub enum SessionEventData { ToolUserRequested(ToolUserRequestedData), #[serde(rename = "tool.execution_start")] ToolExecutionStart(ToolExecutionStartData), + #[doc(hidden)] + #[deprecated] #[serde(rename = "tool.execution_partial_result")] ToolExecutionPartialResult(ToolExecutionPartialResultData), + #[serde(rename = "tool.shell_output")] + ToolShellOutput(ToolShellOutputData), #[serde(rename = "tool.execution_progress")] ToolExecutionProgress(ToolExecutionProgressData), #[serde(rename = "tool.execution_complete")] @@ -962,6 +972,8 @@ pub enum SessionEventData { ExitPlanModeRequested(ExitPlanModeRequestedData), #[serde(rename = "exit_plan_mode.completed")] ExitPlanModeCompleted(ExitPlanModeCompletedData), + #[serde(rename = "human_response.recorded")] + HumanResponseRecorded(HumanResponseRecordedData), #[serde(rename = "session.tools_updated")] SessionToolsUpdated(SessionToolsUpdatedData), #[serde(rename = "session.background_tasks_changed")] @@ -1188,6 +1200,9 @@ pub struct SessionStartData { /// Reasoning effort level used for model calls, if applicable (e.g. "none", "low", "medium", "high", "xhigh", "max") #[serde(skip_serializing_if = "Option::is_none")] pub reasoning_effort: Option, + /// Model that owns effort embedded in an authored model selection. Omitted for independent reasoning-effort overrides and legacy events. + #[serde(skip_serializing_if = "Option::is_none")] + pub reasoning_effort_model: Option, /// Reasoning summary mode used for model calls, if applicable (e.g. "none", "concise", "detailed") #[serde(skip_serializing_if = "Option::is_none")] pub reasoning_summary: Option, @@ -1238,6 +1253,9 @@ pub struct SessionResumeData { /// Reasoning effort level used for model calls, if applicable (e.g. "none", "low", "medium", "high", "xhigh", "max") #[serde(skip_serializing_if = "Option::is_none")] pub reasoning_effort: Option, + /// Model that owns effort embedded in an authored model selection. Omitted for independent reasoning-effort overrides and legacy events. + #[serde(skip_serializing_if = "Option::is_none")] + pub reasoning_effort_model: Option, /// Reasoning summary mode used for model calls, if applicable (e.g. "none", "concise", "detailed") #[serde(skip_serializing_if = "Option::is_none")] pub reasoning_summary: Option, @@ -1534,6 +1552,9 @@ pub struct SessionModelChangeData { /// Reasoning effort level after the model change, if applicable #[serde(skip_serializing_if = "Option::is_none")] pub reasoning_effort: Option, + /// Model that owns effort embedded in an authored model selection. Omitted for independent reasoning-effort overrides and legacy events. + #[serde(skip_serializing_if = "Option::is_none")] + pub reasoning_effort_model: Option, /// Reasoning summary mode after the model change, if applicable #[serde(skip_serializing_if = "Option::is_none")] pub reasoning_summary: Option, @@ -3230,6 +3251,9 @@ pub struct AssistantMessageReasoningBlocks { /// Provider-native reasoning items or content blocks preserved verbatim, in order. A single response can carry several, and provider signatures or identifiers may depend on their exact content and ordering. #[serde(skip_serializing_if = "Option::is_none")] pub blocks: Option>, + /// Anthropic Messages assistant block ordering preserved when the legacy reasoning-only representation cannot reproduce it exactly. Thinking and text blocks remain verbatim; tool-use entries retain identity and a payload fingerprint when later signed reasoning depends on them, and are hydrated from the message's tool requests during replay. + #[serde(skip_serializing_if = "Option::is_none")] + pub ordered_blocks: Option>, /// Model provider that produced these reasoning blocks. pub provider: String, } @@ -4096,16 +4120,33 @@ pub struct ToolExecutionStartData { pub turn_id: Option, } -/// Session event "tool.execution_partial_result". Streaming tool execution output for incremental result display +/// Session event "tool.execution_partial_result". Deprecated merged replacement snapshot of shell output. Use tool.shell_output for append-only, stream-tagged output instead. +#[doc(hidden)] +#[deprecated] #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct ToolExecutionPartialResultData { - /// Incremental output chunk from the running tool + /// Merged replacement snapshot from the running shell, not an append-only chunk pub partial_output: String, /// Tool call ID this partial result belongs to pub tool_call_id: String, } +/// Session event "tool.shell_output". Live, append-only shell output. Not persisted or replayed to late subscribers. Text is decoded and redacted per chunk; chunks need not contain complete lines. +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ToolShellOutputData { + /// Zero-based publication sequence across all output streams for this tool call. Not a byte offset or an OS write-order guarantee. + pub sequence: i64, + /// Output source. Omission means stdout. Terminal output has no separate stdout/stderr attribution. + #[serde(skip_serializing_if = "Option::is_none")] + pub stream: Option, + /// New output to append, without synthetic shell-result markers or stream-switch separators + pub text: String, + /// Tool call ID that owns this shell output + pub tool_call_id: String, +} + /// Session event "tool.execution_progress". Tool execution progress notification with status message #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] @@ -4140,6 +4181,23 @@ pub struct ToolExecutionCompleteError { pub remediation: Option, } +/// A file mutation that was actually committed by a built-in file editing tool. +/// +///
+/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
+#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ToolExecutionCompleteFileEdit { + /// Kind of mutation committed at this path. + pub kind: ToolExecutionCompleteFileEditKind, + /// Absolute path in the session filesystem namespace. + pub path: String, +} + /// Binary result returned by a tool for the model #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] @@ -4603,6 +4661,16 @@ pub struct ToolExecutionCompleteData { /// Error details when the tool execution failed #[serde(skip_serializing_if = "Option::is_none")] pub error: Option, + /// Experimental. File mutations actually committed by a built-in file editing tool, in execution order. Present on successful edits and on partial failures when earlier mutations were committed. Paths are absolute in the session filesystem namespace. + /// + ///
+ /// + /// **Experimental.** This type is part of an experimental wire-protocol surface + /// and may change or be removed in future SDK or CLI releases. + /// + ///
+ #[serde(skip_serializing_if = "Option::is_none")] + pub file_edits: Option>, /// Experimental HydraFusion attribution for this tool completion. /// ///
@@ -7590,6 +7658,79 @@ pub struct ExitPlanModeCompletedData { pub selected_action: Option, } +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct HumanResponseRecordedResponseAskUser { + /// Exact answer content accepted from the user. + pub content: HashMap, + /// Exact question displayed to the user. + pub message: String, + /// Exact response schema displayed to the user. + pub requested_schema: ElicitationRequestedSchema, + /// Runtime-owned response kind discriminator. + pub response_kind: HumanResponseRecordedResponseAskUserResponseKind, +} + +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct HumanResponseRecordedResponseUserInput { + /// Whether the displayed request allowed a free-form answer. + #[serde(skip_serializing_if = "Option::is_none")] + pub allow_freeform: Option, + /// Exact selected or free-form answer submitted by the user. + pub answer: String, + /// Exact choices displayed to the user, when the request offered choices. + #[serde(skip_serializing_if = "Option::is_none")] + pub choices: Option>, + /// Exact question displayed to the user. + pub question: String, + /// Runtime-owned response kind discriminator. + pub response_kind: HumanResponseRecordedResponseUserInputResponseKind, + /// Whether the answer was typed as free-form text rather than selected from the displayed choices. + pub was_freeform: bool, +} + +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct HumanResponseRecordedResponseExitPlanMode { + /// Actions offered by the plan review UI. + pub actions: Vec, + /// Whether the user approved the reviewed plan. + pub approved: bool, + /// Whether the selected response requested edit auto-approval. + #[serde(skip_serializing_if = "Option::is_none")] + pub auto_approve_edits: Option, + /// Exact feedback submitted with the plan decision, when present. + #[serde(skip_serializing_if = "Option::is_none")] + pub feedback: Option, + /// Exact full plan content available from the review UI. + pub plan_content: String, + /// Action the plan review UI recommended. + pub recommended_action: ExitPlanModeAction, + /// Runtime-owned response kind discriminator. + pub response_kind: HumanResponseRecordedResponseExitPlanModeResponseKind, + /// Action selected by the user, when applicable. + #[serde(skip_serializing_if = "Option::is_none")] + pub selected_action: Option, + /// Exact plan summary displayed to the user. + pub summary: String, +} + +/// Session event "human_response.recorded". Durable request-correlated evidence for a typed response to a runtime-owned question or plan review. +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct HumanResponseRecordedData { + /// Controlled actor provenance established at response ingress. + pub actor: HumanResponseActor, + /// Request ID of the runtime-owned question or plan review. + pub request_id: RequestId, + /// Typed request and response payload. + pub response: HumanResponseRecordedResponse, + /// Tool call ID that opened the request, when present. + #[serde(skip_serializing_if = "Option::is_none")] + pub tool_call_id: Option, +} + /// Session event "session.tools_updated". Payload of `session.tools_updated` identifying the model whose resolved tools were updated. #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] @@ -9456,6 +9597,70 @@ pub enum ToolExecutionStartToolDescriptionMetaUIVisibility { Unknown, } +/// Shell output source. Terminal output has no separate stdout/stderr attribution. +#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] +pub enum ToolShellOutputStream { + /// Output from the shell command's standard output stream. This is the default when stream is omitted. + #[serde(rename = "stdout")] + Stdout, + /// Output from the shell command's standard error stream. + #[serde(rename = "stderr")] + Stderr, + /// Inherently merged output that cannot be attributed separately to stdout or stderr. + #[serde(rename = "terminal")] + Terminal, + /// Unknown variant for forward compatibility. + #[default] + #[serde(other)] + Unknown, +} + +/// Kind of file mutation committed by a built-in editing tool. +/// +///
+/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
+#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(from = "String", into = "String")] +pub enum ToolExecutionCompleteFileEditKind { + /// A file was created. + Create, + /// A file was written by an edit operation. + Edit, + /// A file was deleted. + Delete, + /// An unrecognized operation kind, retaining its wire value. + Unknown(String), +} +impl Default for ToolExecutionCompleteFileEditKind { + fn default() -> Self { + Self::Unknown("unknown".to_owned()) + } +} +impl From for ToolExecutionCompleteFileEditKind { + fn from(value: String) -> Self { + match value.as_str() { + "create" => Self::Create, + "edit" => Self::Edit, + "delete" => Self::Delete, + _ => Self::Unknown(value), + } + } +} +impl From for String { + fn from(value: ToolExecutionCompleteFileEditKind) -> Self { + match value { + ToolExecutionCompleteFileEditKind::Create => "create".to_owned(), + ToolExecutionCompleteFileEditKind::Edit => "edit".to_owned(), + ToolExecutionCompleteFileEditKind::Delete => "delete".to_owned(), + ToolExecutionCompleteFileEditKind::Unknown(value) => value, + } + } +} + /// Binary result type discriminator. Use "image" for images and "resource" for other binary data. #[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] pub enum PersistedBinaryImageType { @@ -11207,6 +11412,57 @@ pub enum ExitPlanModeAction { Unknown, } +/// Controlled provenance for a typed runtime response. Only `human_response`, minted by a trusted direct-interaction ingress, is human authorization evidence. +#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] +pub enum HumanResponseActor { + /// A built-in trusted client submitted the response after direct human interaction. + #[serde(rename = "human_response")] + HumanResponse, + /// A host or SDK automation submitted the response without direct human interaction. + #[serde(rename = "host_automation")] + HostAutomation, + /// The response came through a legacy or otherwise unattributed ingress. + #[serde(rename = "unknown")] + UnknownValue, + /// Unknown variant for forward compatibility. + #[default] + #[serde(other)] + Unknown, +} + +/// Runtime-owned response kind discriminator. +#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] +pub enum HumanResponseRecordedResponseAskUserResponseKind { + #[serde(rename = "ask_user")] + #[default] + AskUser, +} + +/// Runtime-owned response kind discriminator. +#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] +pub enum HumanResponseRecordedResponseUserInputResponseKind { + #[serde(rename = "user_input")] + #[default] + UserInput, +} + +/// Runtime-owned response kind discriminator. +#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] +pub enum HumanResponseRecordedResponseExitPlanModeResponseKind { + #[serde(rename = "exit_plan_mode")] + #[default] + ExitPlanMode, +} + +/// Exact runtime-owned question or reviewed plan paired with the typed response that settled it. +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(untagged)] +pub enum HumanResponseRecordedResponse { + AskUser(HumanResponseRecordedResponseAskUser), + UserInput(HumanResponseRecordedResponseUserInput), + ExitPlanMode(HumanResponseRecordedResponseExitPlanMode), +} + /// Terminal status a workflow run committed. A settled run is never `pending` or `running`, so those two members of the run-status domain are deliberately absent. #[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] pub enum WorkflowRunSettledStatus { diff --git a/rust/src/github_token.rs b/rust/src/github_token.rs index e5cd201b83..db77cce96c 100644 --- a/rust/src/github_token.rs +++ b/rust/src/github_token.rs @@ -418,33 +418,4 @@ async fn send_error(client: &Weak, request_id: u64, code: i32, mess } #[cfg(test)] -mod tests { - use super::*; - - #[test] - fn token_debug_is_redacted() { - let token = GitHubToken::new("do-not-print", 28_800); - assert!(!format!("{token:?}").contains("do-not-print")); - } - - #[test] - fn retiring_session_removes_its_provider() { - let registry = GitHubTokenRegistry::new(); - let provider = Arc::new(|_args: GitHubTokenProviderArgs| async { - Ok(GitHubTokenProviderResult::Cancelled) - }); - let registration_id = registry.register(provider); - let session_id = crate::SessionId::from("session-1"); - registry.claim(®istration_id, session_id.clone()); - - registry.retire_session(&session_id); - - assert!( - !registry - .state - .lock() - .providers - .contains_key(®istration_id) - ); - } -} +mod tests; diff --git a/rust/src/github_token/tests.rs b/rust/src/github_token/tests.rs new file mode 100644 index 0000000000..d1376f5360 --- /dev/null +++ b/rust/src/github_token/tests.rs @@ -0,0 +1,34 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +#![cfg(test)] + +use super::*; + +#[test] +fn token_debug_is_redacted() { + let token = GitHubToken::new("do-not-print", 28_800); + assert!(!format!("{token:?}").contains("do-not-print")); +} + +#[test] +fn retiring_session_removes_its_provider() { + let registry = GitHubTokenRegistry::new(); + let provider = Arc::new(|_args: GitHubTokenProviderArgs| async { + Ok(GitHubTokenProviderResult::Cancelled) + }); + let registration_id = registry.register(provider); + let session_id = crate::SessionId::from("session-1"); + registry.claim(®istration_id, session_id.clone()); + + registry.retire_session(&session_id); + + assert!( + !registry + .state + .lock() + .providers + .contains_key(®istration_id) + ); +} diff --git a/rust/src/handler.rs b/rust/src/handler.rs index 61d9b192cb..a21dee9f87 100644 --- a/rust/src/handler.rs +++ b/rust/src/handler.rs @@ -381,97 +381,4 @@ impl PermissionHandler for DenyAllHandler { } #[cfg(test)] -mod tests { - use super::*; - - #[tokio::test] - async fn approve_all_handler_returns_approved() { - let result = ApproveAllHandler - .handle( - SessionId::from("s1"), - RequestId::new("1"), - PermissionRequestData::default(), - ) - .await; - assert!(matches!( - result, - PermissionResult::Decision { - decision: PermissionDecision::ApproveOnce(_), - .. - } - )); - } - - #[tokio::test] - async fn approve_all_handler_fails_when_managed_settings_enabled() { - let result = ApproveAllHandler - .handle( - SessionId::from("s1"), - RequestId::new("1"), - PermissionRequestData { - managed_settings_enabled: true, - ..Default::default() - }, - ) - .await; - assert!(matches!( - result, - PermissionResult::Decision { - decision: PermissionDecision::UserNotAvailable(_), - .. - } - )); - } - - #[tokio::test] - async fn approve_all_handler_leaves_managed_approval_pending() { - let result = ApproveAllHandler - .handle( - SessionId::from("s1"), - RequestId::new("1"), - PermissionRequestData { - managed_approval_required: Some(true), - ..Default::default() - }, - ) - .await; - assert!(matches!(result, PermissionResult::NoResult)); - } - - #[tokio::test] - async fn deny_all_handler_returns_denied() { - let result = DenyAllHandler - .handle( - SessionId::from("s1"), - RequestId::new("1"), - PermissionRequestData::default(), - ) - .await; - assert!(matches!( - result, - PermissionResult::Decision { - decision: PermissionDecision::Reject(_), - .. - } - )); - } - - #[test] - fn mcp_auth_result_token_converts_to_wire_response() { - let wire = McpAuthResult::Token { - access_token: "host-token".to_string(), - token_type: Some("Bearer".to_string()), - expires_in: Some(3600), - } - .into_wire(); - - match wire { - McpOauthPendingRequestResponse::Token(token) => { - assert_eq!(token.access_token, "host-token"); - assert_eq!(token.token_type.as_deref(), Some("Bearer")); - assert_eq!(token.expires_in, Some(3600)); - } - McpOauthPendingRequestResponse::Cancelled(_) => panic!("expected token response"), - } - } -} +mod tests; diff --git a/rust/src/handler/tests.rs b/rust/src/handler/tests.rs new file mode 100644 index 0000000000..1d1df233bc --- /dev/null +++ b/rust/src/handler/tests.rs @@ -0,0 +1,98 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +#![cfg(test)] + +use super::*; + +#[tokio::test] +async fn approve_all_handler_returns_approved() { + let result = ApproveAllHandler + .handle( + SessionId::from("s1"), + RequestId::new("1"), + PermissionRequestData::default(), + ) + .await; + assert!(matches!( + result, + PermissionResult::Decision { + decision: PermissionDecision::ApproveOnce(_), + .. + } + )); +} + +#[tokio::test] +async fn approve_all_handler_fails_when_managed_settings_enabled() { + let result = ApproveAllHandler + .handle( + SessionId::from("s1"), + RequestId::new("1"), + PermissionRequestData { + managed_settings_enabled: true, + ..Default::default() + }, + ) + .await; + assert!(matches!( + result, + PermissionResult::Decision { + decision: PermissionDecision::UserNotAvailable(_), + .. + } + )); +} + +#[tokio::test] +async fn approve_all_handler_leaves_managed_approval_pending() { + let result = ApproveAllHandler + .handle( + SessionId::from("s1"), + RequestId::new("1"), + PermissionRequestData { + managed_approval_required: Some(true), + ..Default::default() + }, + ) + .await; + assert!(matches!(result, PermissionResult::NoResult)); +} + +#[tokio::test] +async fn deny_all_handler_returns_denied() { + let result = DenyAllHandler + .handle( + SessionId::from("s1"), + RequestId::new("1"), + PermissionRequestData::default(), + ) + .await; + assert!(matches!( + result, + PermissionResult::Decision { + decision: PermissionDecision::Reject(_), + .. + } + )); +} + +#[test] +fn mcp_auth_result_token_converts_to_wire_response() { + let wire = McpAuthResult::Token { + access_token: "host-token".to_string(), + token_type: Some("Bearer".to_string()), + expires_in: Some(3600), + } + .into_wire(); + + match wire { + McpOauthPendingRequestResponse::Token(token) => { + assert_eq!(token.access_token, "host-token"); + assert_eq!(token.token_type.as_deref(), Some("Bearer")); + assert_eq!(token.expires_in, Some(3600)); + } + McpOauthPendingRequestResponse::Cancelled(_) => panic!("expected token response"), + } +} diff --git a/rust/src/hooks.rs b/rust/src/hooks.rs index 4986d6cb18..3b2af37cb1 100644 --- a/rust/src/hooks.rs +++ b/rust/src/hooks.rs @@ -362,6 +362,86 @@ pub struct AgentStopOutput { pub reason: Option, } +/// Input for `subagentStart`, received before a subagent's first turn. +#[derive(Debug, Clone, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SubagentStartInput { + /// The parent session ID. + pub session_id: String, + /// Unix timestamp in ms. + pub timestamp: f64, + /// Working directory of the parent session. + #[serde(rename = "cwd")] + pub working_directory: PathBuf, + /// Path to the parent session transcript. + pub transcript_path: PathBuf, + /// Name of the subagent definition. + pub agent_name: String, + /// Display name, when the definition provides one. + #[serde(default)] + pub agent_display_name: Option, + /// Description, when the definition provides one. + #[serde(default)] + pub agent_description: Option, +} + +/// Output for `subagentStart`. +#[derive(Debug, Clone, Default, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct SubagentStartOutput { + /// Context prepended to the subagent's first prompt. + #[serde(skip_serializing_if = "Option::is_none")] + pub additional_context: Option, +} + +/// Input for `subagentStop`, received after a subagent's turn. +#[derive(Debug, Clone, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SubagentStopInput { + /// The parent session ID. + pub session_id: String, + /// Unix timestamp in ms. + pub timestamp: f64, + /// Working directory of the parent session. + #[serde(rename = "cwd")] + pub working_directory: PathBuf, + /// Path to the parent session transcript. + pub transcript_path: PathBuf, + /// Name of the subagent definition. + pub agent_name: String, + /// Type of the subagent. + pub agent_type: String, + /// Agent ID, when available. + #[serde(default)] + pub agent_id: Option, + /// Display name, when the definition provides one. + #[serde(default)] + pub agent_display_name: Option, + /// Description, when the definition provides one. + #[serde(default)] + pub agent_description: Option, + /// Reason the subagent stopped (normally `"end_turn"`). + pub stop_reason: String, + /// The subagent's last assistant response. + pub response: String, +} + +/// Output for `subagentStop`. +#[derive(Debug, Clone, Default, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct SubagentStopOutput { + /// Set to `"block"` to run another subagent turn; only `"allow"` and `"block"` are valid. + #[serde(skip_serializing_if = "Option::is_none")] + pub decision: Option, + /// Nonempty follow-up instruction required when blocking; without a block + /// decision this is an error, not an instruction to the child. + #[serde(skip_serializing_if = "Option::is_none")] + pub reason: Option, + /// Replacement final response when the stop is allowed. + #[serde(skip_serializing_if = "Option::is_none")] + pub modified_response: Option, +} + /// Events dispatched to [`SessionHooks::on_hook`] at CLI lifecycle points. /// /// Each variant carries the typed input for that hook plus the shared @@ -442,6 +522,20 @@ pub enum HookEvent { /// Session context. ctx: HookContext, }, + /// Fired before a subagent's first turn. + SubagentStart { + /// Typed input data. + input: SubagentStartInput, + /// Session context. + ctx: HookContext, + }, + /// Fired after a subagent's turn. + SubagentStop { + /// Typed input data. + input: SubagentStopInput, + /// Session context. + ctx: HookContext, + }, } /// Response from [`SessionHooks::on_hook`] back to the SDK. @@ -473,6 +567,10 @@ pub enum HookOutput { ErrorOccurred(ErrorOccurredOutput), /// Response for an agent-stop hook. AgentStop(AgentStopOutput), + /// Response for a subagent-start hook. + SubagentStart(SubagentStartOutput), + /// Response for a subagent-stop hook. + SubagentStop(SubagentStopOutput), } impl HookOutput { @@ -489,6 +587,8 @@ impl HookOutput { Self::SessionEnd(_) => "SessionEnd", Self::ErrorOccurred(_) => "ErrorOccurred", Self::AgentStop(_) => "AgentStop", + Self::SubagentStart(_) => "SubagentStart", + Self::SubagentStop(_) => "SubagentStop", } } } @@ -567,6 +667,16 @@ pub trait SessionHooks: Send + Sync + 'static { .await .map(HookOutput::AgentStop) .unwrap_or(HookOutput::None), + HookEvent::SubagentStart { input, ctx } => self + .on_subagent_start(input, ctx) + .await + .map(HookOutput::SubagentStart) + .unwrap_or(HookOutput::None), + HookEvent::SubagentStop { input, ctx } => self + .on_subagent_stop(input, ctx) + .await + .map(HookOutput::SubagentStop) + .unwrap_or(HookOutput::None), } } @@ -673,6 +783,25 @@ pub trait SessionHooks: Send + Sync + 'static { ) -> Option { None } + + /// Called before a subagent runs. Return context to prepend to its prompt. + async fn on_subagent_start( + &self, + _input: SubagentStartInput, + _ctx: HookContext, + ) -> Option { + None + } + + /// Called when a subagent stops. Return a block decision with a reason + /// to continue it, or a replacement for its final response. + async fn on_subagent_stop( + &self, + _input: SubagentStopInput, + _ctx: HookContext, + ) -> Option { + None + } } /// Dispatches a `hooks.invoke` request to [`SessionHooks::on_hook`]. @@ -731,6 +860,14 @@ pub(crate) async fn dispatch_hook( let input: AgentStopInput = serde_json::from_value(raw_input)?; HookEvent::AgentStop { input, ctx } } + "subagentStart" => { + let input: SubagentStartInput = serde_json::from_value(raw_input)?; + HookEvent::SubagentStart { input, ctx } + } + "subagentStop" => { + let input: SubagentStopInput = serde_json::from_value(raw_input)?; + HookEvent::SubagentStop { input, ctx } + } _ => { tracing::warn!( hook_type = hook_type, @@ -770,6 +907,8 @@ pub(crate) async fn dispatch_hook( ("sessionEnd", HookOutput::SessionEnd(o)) => Some(serde_json::to_value(o)?), ("errorOccurred", HookOutput::ErrorOccurred(o)) => Some(serde_json::to_value(o)?), ("agentStop", HookOutput::AgentStop(o)) => Some(serde_json::to_value(o)?), + ("subagentStart", HookOutput::SubagentStart(o)) => Some(serde_json::to_value(o)?), + ("subagentStop", HookOutput::SubagentStop(o)) => Some(serde_json::to_value(o)?), _ => { tracing::warn!( hook_type = hook_type, @@ -785,400 +924,4 @@ pub(crate) async fn dispatch_hook( } #[cfg(test)] -mod tests { - use super::*; - - struct TestHooks; - - #[async_trait] - impl SessionHooks for TestHooks { - async fn on_hook(&self, event: HookEvent) -> HookOutput { - match event { - HookEvent::PreToolUse { input, .. } => { - if input.tool_name == "dangerous_tool" { - HookOutput::PreToolUse(PreToolUseOutput { - permission_decision: Some("deny".to_string()), - permission_decision_reason: Some("blocked by policy".to_string()), - ..Default::default() - }) - } else { - HookOutput::None - } - } - HookEvent::UserPromptSubmitted { input, .. } => { - HookOutput::UserPromptSubmitted(UserPromptSubmittedOutput { - modified_prompt: Some(format!("[prefixed] {}", input.prompt)), - ..Default::default() - }) - } - HookEvent::UserPromptTransformed { input, .. } => { - HookOutput::UserPromptTransformed(UserPromptTransformedOutput { - modified_transformed_prompt: Some(format!( - "[transformed] {}", - input.transformed_prompt - )), - }) - } - _ => HookOutput::None, - } - } - } - - #[tokio::test] - async fn dispatch_pre_tool_use_deny() { - let hooks = TestHooks; - let input = serde_json::json!({ - "sessionId": "sess-1", - "timestamp": 1234567890, - "cwd": "/tmp", - "toolName": "dangerous_tool", - "toolArgs": {} - }); - let result = dispatch_hook(&hooks, &SessionId::new("sess-1"), "preToolUse", input) - .await - .unwrap(); - let output = &result["output"]; - assert_eq!(output["permissionDecision"], "deny"); - assert_eq!(output["permissionDecisionReason"], "blocked by policy"); - } - - #[tokio::test] - async fn dispatch_pre_tool_use_passthrough() { - let hooks = TestHooks; - let input = serde_json::json!({ - "sessionId": "sess-1", - "timestamp": 1234567890, - "cwd": "/tmp", - "toolName": "safe_tool", - "toolArgs": {"key": "value"} - }); - let result = dispatch_hook(&hooks, &SessionId::new("sess-1"), "preToolUse", input) - .await - .unwrap(); - // No hook registered for this tool — output should be empty object - assert_eq!(result["output"], serde_json::json!({})); - } - - #[tokio::test] - async fn dispatch_user_prompt_submitted() { - let hooks = TestHooks; - let input = serde_json::json!({ - "sessionId": "sess-1", - "timestamp": 1234567890, - "cwd": "/tmp", - "prompt": "hello world" - }); - let result = dispatch_hook( - &hooks, - &SessionId::new("sess-1"), - "userPromptSubmitted", - input, - ) - .await - .unwrap(); - assert_eq!(result["output"]["modifiedPrompt"], "[prefixed] hello world"); - } - - #[tokio::test] - async fn dispatch_user_prompt_transformed() { - let hooks = TestHooks; - let input = serde_json::json!({ - "sessionId": "sess-1", - "timestamp": 1234567890, - "cwd": "/tmp", - "prompt": "hello world", - "transformedPrompt": "now\nhello world" - }); - let result = dispatch_hook( - &hooks, - &SessionId::new("sess-1"), - "userPromptTransformed", - input, - ) - .await - .unwrap(); - assert_eq!( - result["output"]["modifiedTransformedPrompt"], - "[transformed] now\nhello world" - ); - } - - #[tokio::test] - async fn dispatch_unregistered_hook_returns_empty() { - let hooks = TestHooks; - let input = serde_json::json!({ - "sessionId": "sess-1", - "timestamp": 1234567890, - "cwd": "/tmp", - "reason": "complete" - }); - // TestHooks doesn't handle SessionEnd - let result = dispatch_hook(&hooks, &SessionId::new("sess-1"), "sessionEnd", input) - .await - .unwrap(); - assert_eq!(result["output"], serde_json::json!({})); - } - - #[tokio::test] - async fn dispatch_unknown_hook_type() { - let hooks = TestHooks; - let input = serde_json::json!({}); - let result = dispatch_hook(&hooks, &SessionId::new("sess-1"), "unknownHook", input) - .await - .unwrap(); - assert_eq!(result["output"], serde_json::json!({})); - } - - #[tokio::test] - async fn dispatch_mismatched_output_returns_empty() { - struct MismatchHooks; - #[async_trait] - impl SessionHooks for MismatchHooks { - async fn on_hook(&self, _event: HookEvent) -> HookOutput { - // Always return SessionEnd output regardless of event type - HookOutput::SessionEnd(SessionEndOutput { - session_summary: Some("oops".to_string()), - ..Default::default() - }) - } - } - - let hooks = MismatchHooks; - let input = serde_json::json!({ - "sessionId": "sess-1", - "timestamp": 1234567890, - "cwd": "/tmp", - "toolName": "some_tool", - "toolArgs": {} - }); - // preToolUse event gets a SessionEnd output — should be treated as empty - let result = dispatch_hook(&hooks, &SessionId::new("sess-1"), "preToolUse", input) - .await - .unwrap(); - assert_eq!(result["output"], serde_json::json!({})); - } - - #[tokio::test] - async fn dispatch_post_tool_use_default() { - let hooks = TestHooks; - let input = serde_json::json!({ - "sessionId": "sess-1", - "timestamp": 1234567890, - "cwd": "/tmp", - "toolName": "some_tool", - "toolArgs": {}, - "toolResult": "success" - }); - let result = dispatch_hook(&hooks, &SessionId::new("sess-1"), "postToolUse", input) - .await - .unwrap(); - assert_eq!(result["output"], serde_json::json!({})); - } - - #[tokio::test] - async fn dispatch_post_tool_use_failure_default() { - // No handler override — should return an empty output object. - let hooks = TestHooks; - let input = serde_json::json!({ - "sessionId": "sess-1", - "timestamp": 1234567890, - "cwd": "/tmp", - "toolName": "some_tool", - "toolArgs": {"key": "value"}, - "error": "boom" - }); - let result = dispatch_hook( - &hooks, - &SessionId::new("sess-1"), - "postToolUseFailure", - input, - ) - .await - .unwrap(); - assert_eq!(result["output"], serde_json::json!({})); - } - - #[tokio::test] - async fn dispatch_post_tool_use_failure_returns_additional_context() { - struct FailureHooks; - #[async_trait] - impl SessionHooks for FailureHooks { - async fn on_post_tool_use_failure( - &self, - input: PostToolUseFailureInput, - _ctx: HookContext, - ) -> Option { - assert_eq!(input.session_id, "sess-1"); - assert_eq!(input.tool_name, "some_tool"); - assert_eq!(input.error, "boom"); - assert_eq!(input.working_directory, PathBuf::from("/tmp")); - Some(PostToolUseFailureOutput { - additional_context: Some(format!( - "tool {} failed: {}", - input.tool_name, input.error - )), - }) - } - } - - let input = serde_json::json!({ - "sessionId": "sess-1", - "timestamp": 1234567890, - "cwd": "/tmp", - "toolName": "some_tool", - "toolArgs": {}, - "error": "boom" - }); - let result = dispatch_hook( - &FailureHooks, - &SessionId::new("sess-1"), - "postToolUseFailure", - input, - ) - .await - .unwrap(); - assert_eq!( - result["output"]["additionalContext"], - "tool some_tool failed: boom" - ); - } - - #[tokio::test] - async fn dispatch_post_tool_use_failure_invalid_input_errors() { - // Missing required `error` field — dispatcher should surface the - // deserialization error rather than dispatching with empty input. - let hooks = TestHooks; - let input = serde_json::json!({ - "sessionId": "sess-1", - "timestamp": 1234567890, - "cwd": "/tmp", - "toolName": "some_tool", - "toolArgs": {} - }); - let err = dispatch_hook( - &hooks, - &SessionId::new("sess-1"), - "postToolUseFailure", - input, - ) - .await - .unwrap_err(); - let msg = err.to_string().to_ascii_lowercase(); - assert!( - msg.contains("error") || msg.contains("missing field"), - "unexpected error: {msg}" - ); - } - - #[tokio::test] - async fn dispatch_session_start() { - struct StartHooks; - #[async_trait] - impl SessionHooks for StartHooks { - async fn on_hook(&self, event: HookEvent) -> HookOutput { - match event { - HookEvent::SessionStart { .. } => { - HookOutput::SessionStart(SessionStartOutput { - additional_context: Some("extra context".to_string()), - ..Default::default() - }) - } - _ => HookOutput::None, - } - } - } - - let hooks = StartHooks; - let input = serde_json::json!({ - "sessionId": "sess-1", - "timestamp": 1234567890, - "cwd": "/tmp", - "source": "new" - }); - let result = dispatch_hook(&hooks, &SessionId::new("sess-1"), "sessionStart", input) - .await - .unwrap(); - assert_eq!(result["output"]["additionalContext"], "extra context"); - } - - #[tokio::test] - async fn dispatch_error_occurred() { - struct ErrorHooks; - #[async_trait] - impl SessionHooks for ErrorHooks { - async fn on_hook(&self, event: HookEvent) -> HookOutput { - match event { - HookEvent::ErrorOccurred { .. } => { - HookOutput::ErrorOccurred(ErrorOccurredOutput { - error_handling: Some("retry".to_string()), - retry_count: Some(3), - ..Default::default() - }) - } - _ => HookOutput::None, - } - } - } - - let hooks = ErrorHooks; - let input = serde_json::json!({ - "sessionId": "sess-1", - "timestamp": 1234567890, - "cwd": "/tmp", - "error": "model timeout", - "errorContext": "model_call", - "recoverable": true - }); - let result = dispatch_hook(&hooks, &SessionId::new("sess-1"), "errorOccurred", input) - .await - .unwrap(); - assert_eq!(result["output"]["errorHandling"], "retry"); - assert_eq!(result["output"]["retryCount"], 3); - } - - #[tokio::test] - async fn dispatch_agent_stop_block() { - struct AgentStopHooks; - #[async_trait] - impl SessionHooks for AgentStopHooks { - async fn on_agent_stop( - &self, - input: AgentStopInput, - ctx: HookContext, - ) -> Option { - assert_eq!(ctx.session_id, SessionId::new("sess-1")); - assert_eq!(input.session_id, "sess-1"); - assert_eq!(input.stop_reason.as_deref(), Some("end_turn")); - assert_eq!( - input.transcript_path, - Some(PathBuf::from("/tmp/transcript.jsonl")) - ); - assert_eq!(input.stop_hook_active, Some(true)); - Some(AgentStopOutput { - decision: Some("block".to_string()), - reason: Some("finish the remaining work".to_string()), - }) - } - } - - let input = serde_json::json!({ - "sessionId": "sess-1", - "timestamp": 1234567890, - "cwd": "/tmp", - "stopReason": "end_turn", - "transcriptPath": "/tmp/transcript.jsonl", - "stop_hook_active": true - }); - let result = dispatch_hook( - &AgentStopHooks, - &SessionId::new("sess-1"), - "agentStop", - input, - ) - .await - .unwrap(); - - assert_eq!(result["output"]["decision"], "block"); - assert_eq!(result["output"]["reason"], "finish the remaining work"); - } -} +mod tests; diff --git a/rust/src/hooks/tests.rs b/rust/src/hooks/tests.rs new file mode 100644 index 0000000000..e8a8179c41 --- /dev/null +++ b/rust/src/hooks/tests.rs @@ -0,0 +1,530 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +#![cfg(test)] + +use super::*; + +struct TestHooks; + +#[async_trait] +impl SessionHooks for TestHooks { + async fn on_hook(&self, event: HookEvent) -> HookOutput { + match event { + HookEvent::PreToolUse { input, .. } => { + if input.tool_name == "dangerous_tool" { + HookOutput::PreToolUse(PreToolUseOutput { + permission_decision: Some("deny".to_string()), + permission_decision_reason: Some("blocked by policy".to_string()), + ..Default::default() + }) + } else { + HookOutput::None + } + } + HookEvent::UserPromptSubmitted { input, .. } => { + HookOutput::UserPromptSubmitted(UserPromptSubmittedOutput { + modified_prompt: Some(format!("[prefixed] {}", input.prompt)), + ..Default::default() + }) + } + HookEvent::UserPromptTransformed { input, .. } => { + HookOutput::UserPromptTransformed(UserPromptTransformedOutput { + modified_transformed_prompt: Some(format!( + "[transformed] {}", + input.transformed_prompt + )), + }) + } + _ => HookOutput::None, + } + } +} + +#[tokio::test] +async fn dispatch_pre_tool_use_deny() { + let hooks = TestHooks; + let input = serde_json::json!({ + "sessionId": "sess-1", + "timestamp": 1234567890, + "cwd": "/tmp", + "toolName": "dangerous_tool", + "toolArgs": {} + }); + let result = dispatch_hook(&hooks, &SessionId::new("sess-1"), "preToolUse", input) + .await + .unwrap(); + let output = &result["output"]; + assert_eq!(output["permissionDecision"], "deny"); + assert_eq!(output["permissionDecisionReason"], "blocked by policy"); +} + +#[tokio::test] +async fn dispatch_pre_tool_use_passthrough() { + let hooks = TestHooks; + let input = serde_json::json!({ + "sessionId": "sess-1", + "timestamp": 1234567890, + "cwd": "/tmp", + "toolName": "safe_tool", + "toolArgs": {"key": "value"} + }); + let result = dispatch_hook(&hooks, &SessionId::new("sess-1"), "preToolUse", input) + .await + .unwrap(); + // No hook registered for this tool — output should be empty object + assert_eq!(result["output"], serde_json::json!({})); +} + +#[tokio::test] +async fn dispatch_user_prompt_submitted() { + let hooks = TestHooks; + let input = serde_json::json!({ + "sessionId": "sess-1", + "timestamp": 1234567890, + "cwd": "/tmp", + "prompt": "hello world" + }); + let result = dispatch_hook( + &hooks, + &SessionId::new("sess-1"), + "userPromptSubmitted", + input, + ) + .await + .unwrap(); + assert_eq!(result["output"]["modifiedPrompt"], "[prefixed] hello world"); +} + +#[tokio::test] +async fn dispatch_user_prompt_transformed() { + let hooks = TestHooks; + let input = serde_json::json!({ + "sessionId": "sess-1", + "timestamp": 1234567890, + "cwd": "/tmp", + "prompt": "hello world", + "transformedPrompt": "now\nhello world" + }); + let result = dispatch_hook( + &hooks, + &SessionId::new("sess-1"), + "userPromptTransformed", + input, + ) + .await + .unwrap(); + assert_eq!( + result["output"]["modifiedTransformedPrompt"], + "[transformed] now\nhello world" + ); +} + +#[tokio::test] +async fn dispatch_unregistered_hook_returns_empty() { + let hooks = TestHooks; + let input = serde_json::json!({ + "sessionId": "sess-1", + "timestamp": 1234567890, + "cwd": "/tmp", + "reason": "complete" + }); + // TestHooks doesn't handle SessionEnd + let result = dispatch_hook(&hooks, &SessionId::new("sess-1"), "sessionEnd", input) + .await + .unwrap(); + assert_eq!(result["output"], serde_json::json!({})); +} + +#[tokio::test] +async fn dispatch_unknown_hook_type() { + let hooks = TestHooks; + let input = serde_json::json!({}); + let result = dispatch_hook(&hooks, &SessionId::new("sess-1"), "unknownHook", input) + .await + .unwrap(); + assert_eq!(result["output"], serde_json::json!({})); +} + +#[tokio::test] +async fn dispatch_subagent_hooks_with_typed_input_and_output() { + struct SubagentHooks; + #[async_trait] + impl SessionHooks for SubagentHooks { + async fn on_subagent_start( + &self, + input: SubagentStartInput, + ctx: HookContext, + ) -> Option { + assert_eq!(ctx.session_id, SessionId::new("sess-1")); + assert_eq!(input.session_id, "sess-1"); + assert_eq!(input.timestamp, 1234567890.0); + assert_eq!(input.working_directory, PathBuf::from("/tmp")); + assert_eq!( + input.transcript_path, + PathBuf::from("/tmp/transcript.jsonl") + ); + assert_eq!(input.agent_name, "explore"); + assert_eq!(input.agent_display_name.as_deref(), Some("Explore Agent")); + assert_eq!(input.agent_description.as_deref(), Some("Read code")); + Some(SubagentStartOutput { + additional_context: Some("Follow the file".to_string()), + }) + } + + async fn on_subagent_stop( + &self, + input: SubagentStopInput, + ctx: HookContext, + ) -> Option { + assert_eq!(ctx.session_id, SessionId::new("sess-1")); + assert_eq!(input.session_id, "sess-1"); + assert_eq!(input.timestamp, 1234567891.0); + assert_eq!(input.working_directory, PathBuf::from("/tmp")); + assert_eq!( + input.transcript_path, + PathBuf::from("/tmp/transcript.jsonl") + ); + assert_eq!(input.agent_name, "explore"); + assert_eq!(input.agent_type, "explore"); + assert_eq!(input.agent_id.as_deref(), Some("read-file")); + assert_eq!(input.agent_display_name.as_deref(), Some("Explore Agent")); + assert_eq!(input.agent_description.as_deref(), Some("Read code")); + assert_eq!(input.stop_reason, "end_turn"); + assert_eq!(input.response, "original answer"); + Some(SubagentStopOutput { + modified_response: Some("rewritten answer".to_string()), + ..Default::default() + }) + } + } + + let common = serde_json::json!({ + "sessionId": "sess-1", + "timestamp": 1234567890, + "cwd": "/tmp", + "transcriptPath": "/tmp/transcript.jsonl", + "agentName": "explore", + "agentDisplayName": "Explore Agent", + "agentDescription": "Read code" + }); + let start = dispatch_hook( + &SubagentHooks, + &SessionId::new("sess-1"), + "subagentStart", + common.clone(), + ) + .await + .unwrap(); + assert_eq!( + start, + serde_json::json!({ "output": { "additionalContext": "Follow the file" } }) + ); + + let mut stop = common; + let object = stop.as_object_mut().unwrap(); + object.insert("timestamp".to_string(), serde_json::json!(1234567891)); + object.insert("agentType".to_string(), serde_json::json!("explore")); + object.insert("agentId".to_string(), serde_json::json!("read-file")); + object.insert("stopReason".to_string(), serde_json::json!("end_turn")); + object.insert("response".to_string(), serde_json::json!("original answer")); + let stopped = dispatch_hook( + &SubagentHooks, + &SessionId::new("sess-1"), + "subagentStop", + stop, + ) + .await + .unwrap(); + assert_eq!( + stopped, + serde_json::json!({ "output": { "modifiedResponse": "rewritten answer" } }) + ); +} + +#[tokio::test] +async fn dispatch_subagent_stop_block_output() { + struct BlockHook; + #[async_trait] + impl SessionHooks for BlockHook { + async fn on_subagent_stop( + &self, + _input: SubagentStopInput, + _ctx: HookContext, + ) -> Option { + Some(SubagentStopOutput { + decision: Some("block".to_string()), + reason: Some("Keep researching".to_string()), + ..Default::default() + }) + } + } + + let input = serde_json::json!({ + "sessionId": "sess-1", + "timestamp": 1234567890, + "cwd": "/tmp", + "transcriptPath": "/tmp/transcript.jsonl", + "agentName": "explore", + "agentType": "explore", + "stopReason": "end_turn", + "response": "draft answer" + }); + let result = dispatch_hook(&BlockHook, &SessionId::new("sess-1"), "subagentStop", input) + .await + .unwrap(); + assert_eq!( + result, + serde_json::json!({ "output": { "decision": "block", "reason": "Keep researching" } }) + ); +} + +#[tokio::test] +async fn dispatch_mismatched_output_returns_empty() { + struct MismatchHooks; + #[async_trait] + impl SessionHooks for MismatchHooks { + async fn on_hook(&self, _event: HookEvent) -> HookOutput { + // Always return SessionEnd output regardless of event type + HookOutput::SessionEnd(SessionEndOutput { + session_summary: Some("oops".to_string()), + ..Default::default() + }) + } + } + + let hooks = MismatchHooks; + let input = serde_json::json!({ + "sessionId": "sess-1", + "timestamp": 1234567890, + "cwd": "/tmp", + "toolName": "some_tool", + "toolArgs": {} + }); + // preToolUse event gets a SessionEnd output — should be treated as empty + let result = dispatch_hook(&hooks, &SessionId::new("sess-1"), "preToolUse", input) + .await + .unwrap(); + assert_eq!(result["output"], serde_json::json!({})); +} + +#[tokio::test] +async fn dispatch_post_tool_use_default() { + let hooks = TestHooks; + let input = serde_json::json!({ + "sessionId": "sess-1", + "timestamp": 1234567890, + "cwd": "/tmp", + "toolName": "some_tool", + "toolArgs": {}, + "toolResult": "success" + }); + let result = dispatch_hook(&hooks, &SessionId::new("sess-1"), "postToolUse", input) + .await + .unwrap(); + assert_eq!(result["output"], serde_json::json!({})); +} + +#[tokio::test] +async fn dispatch_post_tool_use_failure_default() { + // No handler override — should return an empty output object. + let hooks = TestHooks; + let input = serde_json::json!({ + "sessionId": "sess-1", + "timestamp": 1234567890, + "cwd": "/tmp", + "toolName": "some_tool", + "toolArgs": {"key": "value"}, + "error": "boom" + }); + let result = dispatch_hook( + &hooks, + &SessionId::new("sess-1"), + "postToolUseFailure", + input, + ) + .await + .unwrap(); + assert_eq!(result["output"], serde_json::json!({})); +} + +#[tokio::test] +async fn dispatch_post_tool_use_failure_returns_additional_context() { + struct FailureHooks; + #[async_trait] + impl SessionHooks for FailureHooks { + async fn on_post_tool_use_failure( + &self, + input: PostToolUseFailureInput, + _ctx: HookContext, + ) -> Option { + assert_eq!(input.session_id, "sess-1"); + assert_eq!(input.tool_name, "some_tool"); + assert_eq!(input.error, "boom"); + assert_eq!(input.working_directory, PathBuf::from("/tmp")); + Some(PostToolUseFailureOutput { + additional_context: Some(format!( + "tool {} failed: {}", + input.tool_name, input.error + )), + }) + } + } + + let input = serde_json::json!({ + "sessionId": "sess-1", + "timestamp": 1234567890, + "cwd": "/tmp", + "toolName": "some_tool", + "toolArgs": {}, + "error": "boom" + }); + let result = dispatch_hook( + &FailureHooks, + &SessionId::new("sess-1"), + "postToolUseFailure", + input, + ) + .await + .unwrap(); + assert_eq!( + result["output"]["additionalContext"], + "tool some_tool failed: boom" + ); +} + +#[tokio::test] +async fn dispatch_post_tool_use_failure_invalid_input_errors() { + // Missing required `error` field — dispatcher should surface the + // deserialization error rather than dispatching with empty input. + let hooks = TestHooks; + let input = serde_json::json!({ + "sessionId": "sess-1", + "timestamp": 1234567890, + "cwd": "/tmp", + "toolName": "some_tool", + "toolArgs": {} + }); + let err = dispatch_hook( + &hooks, + &SessionId::new("sess-1"), + "postToolUseFailure", + input, + ) + .await + .unwrap_err(); + let msg = err.to_string().to_ascii_lowercase(); + assert!( + msg.contains("error") || msg.contains("missing field"), + "unexpected error: {msg}" + ); +} + +#[tokio::test] +async fn dispatch_session_start() { + struct StartHooks; + #[async_trait] + impl SessionHooks for StartHooks { + async fn on_hook(&self, event: HookEvent) -> HookOutput { + match event { + HookEvent::SessionStart { .. } => HookOutput::SessionStart(SessionStartOutput { + additional_context: Some("extra context".to_string()), + ..Default::default() + }), + _ => HookOutput::None, + } + } + } + + let hooks = StartHooks; + let input = serde_json::json!({ + "sessionId": "sess-1", + "timestamp": 1234567890, + "cwd": "/tmp", + "source": "new" + }); + let result = dispatch_hook(&hooks, &SessionId::new("sess-1"), "sessionStart", input) + .await + .unwrap(); + assert_eq!(result["output"]["additionalContext"], "extra context"); +} + +#[tokio::test] +async fn dispatch_error_occurred() { + struct ErrorHooks; + #[async_trait] + impl SessionHooks for ErrorHooks { + async fn on_hook(&self, event: HookEvent) -> HookOutput { + match event { + HookEvent::ErrorOccurred { .. } => HookOutput::ErrorOccurred(ErrorOccurredOutput { + error_handling: Some("retry".to_string()), + retry_count: Some(3), + ..Default::default() + }), + _ => HookOutput::None, + } + } + } + + let hooks = ErrorHooks; + let input = serde_json::json!({ + "sessionId": "sess-1", + "timestamp": 1234567890, + "cwd": "/tmp", + "error": "model timeout", + "errorContext": "model_call", + "recoverable": true + }); + let result = dispatch_hook(&hooks, &SessionId::new("sess-1"), "errorOccurred", input) + .await + .unwrap(); + assert_eq!(result["output"]["errorHandling"], "retry"); + assert_eq!(result["output"]["retryCount"], 3); +} + +#[tokio::test] +async fn dispatch_agent_stop_block() { + struct AgentStopHooks; + #[async_trait] + impl SessionHooks for AgentStopHooks { + async fn on_agent_stop( + &self, + input: AgentStopInput, + ctx: HookContext, + ) -> Option { + assert_eq!(ctx.session_id, SessionId::new("sess-1")); + assert_eq!(input.session_id, "sess-1"); + assert_eq!(input.stop_reason.as_deref(), Some("end_turn")); + assert_eq!( + input.transcript_path, + Some(PathBuf::from("/tmp/transcript.jsonl")) + ); + assert_eq!(input.stop_hook_active, Some(true)); + Some(AgentStopOutput { + decision: Some("block".to_string()), + reason: Some("finish the remaining work".to_string()), + }) + } + } + + let input = serde_json::json!({ + "sessionId": "sess-1", + "timestamp": 1234567890, + "cwd": "/tmp", + "stopReason": "end_turn", + "transcriptPath": "/tmp/transcript.jsonl", + "stop_hook_active": true + }); + let result = dispatch_hook( + &AgentStopHooks, + &SessionId::new("sess-1"), + "agentStop", + input, + ) + .await + .unwrap(); + + assert_eq!(result["output"]["decision"], "block"); + assert_eq!(result["output"]["reason"], "finish the remaining work"); +} diff --git a/rust/src/jsonrpc.rs b/rust/src/jsonrpc.rs index 32cd242b77..8da2c72fb5 100644 --- a/rust/src/jsonrpc.rs +++ b/rust/src/jsonrpc.rs @@ -889,161 +889,4 @@ impl Drop for PendingGuard<'_> { } #[cfg(test)] -mod tests { - use super::*; - - #[test] - fn listener_negotiation_logs_do_not_echo_remote_errors() { - let message = "request rejected: token=listener-secret-sentinel"; - for method in ["host.getConfiguration", "host.ready"] { - assert_eq!( - remote_error_log_message(method, message), - "listener negotiation request rejected" - ); - } - assert_eq!(remote_error_log_message("ping", message), message); - } - - #[test] - fn deserialize_notification() { - let json = r#"{"jsonrpc":"2.0","method":"session.event","params":{"id":"e1"}}"#; - let msg: JsonRpcMessage = serde_json::from_str(json).unwrap(); - assert!(matches!(msg, JsonRpcMessage::Notification(n) if n.method == "session.event")); - } - - #[test] - fn deserialize_request() { - let json = - r#"{"jsonrpc":"2.0","id":5,"method":"permission.request","params":{"kind":"shell"}}"#; - let msg: JsonRpcMessage = serde_json::from_str(json).unwrap(); - assert!( - matches!(msg, JsonRpcMessage::Request(r) if r.id == 5 && r.method == "permission.request") - ); - } - - #[test] - fn deserialize_response_with_result() { - let json = r#"{"jsonrpc":"2.0","id":3,"result":{"ok":true}}"#; - let msg: JsonRpcMessage = serde_json::from_str(json).unwrap(); - assert!(matches!(msg, JsonRpcMessage::Response(r) if r.id == 3 && !r.is_error())); - } - - #[test] - fn deserialize_error_response() { - let json = r#"{"jsonrpc":"2.0","id":7,"error":{"code":-32600,"message":"Invalid Request","data":{"nested":[1,{"reason":"invalid"}]}}}"#; - let msg: JsonRpcMessage = serde_json::from_str(json).unwrap(); - match msg { - JsonRpcMessage::Response(r) => { - assert!(r.is_error()); - let err = r.error.unwrap(); - assert_eq!(err.code, -32600); - assert_eq!(err.message, "Invalid Request"); - assert_eq!( - err.data, - Some(serde_json::json!({"nested": [1, {"reason": "invalid"}]})) - ); - } - other => panic!("expected Response, got {other:?}"), - } - } - - #[test] - fn deserialize_rejects_non_object() { - let result = serde_json::from_str::(r#""not an object""#); - assert!(result.is_err()); - } - - #[test] - fn deserialize_preserves_optional_payloads() { - for payload in [ - None, - Some(Value::Null), - Some(serde_json::json!(false)), - Some(serde_json::json!(42)), - Some(serde_json::json!("text")), - Some(serde_json::json!([{"nested": [1, null, true]}])), - Some(serde_json::json!({"rows": [{"content": "result"}]})), - ] { - for mut envelope in [ - serde_json::json!({"jsonrpc": "2.0", "method": "notify"}), - serde_json::json!({"jsonrpc": "2.0", "id": 1, "method": "request"}), - serde_json::json!({"jsonrpc": "2.0", "id": 1}), - ] { - let (payload_key, ignored_key) = if envelope.get("method").is_some() { - ("params", "result") - } else { - ("result", "params") - }; - envelope[ignored_key] = serde_json::json!({"ignored": "opposite payload"}); - if let Some(payload) = &payload { - envelope[payload_key] = payload.clone(); - } - let actual = match serde_json::from_value::(envelope).unwrap() { - JsonRpcMessage::Request(request) => request.params, - JsonRpcMessage::Response(response) => response.result, - JsonRpcMessage::Notification(notification) => notification.params, - }; - assert_eq!(actual, payload.clone().filter(|value| !value.is_null())); - } - } - } - - #[test] - fn deserialize_rejects_invalid_metadata() { - for json in [ - r#"{"jsonrpc":null,"method":"notify","params":{"nested":[1]}}"#, - r#"{"jsonrpc":"2.0","method":42,"params":{"nested":[1]}}"#, - r#"{"jsonrpc":"2.0","id":null,"result":{}}"#, - r#"{"jsonrpc":"2.0","id":"1","result":{}}"#, - r#"{"jsonrpc":"2.0","id":-1,"result":{}}"#, - r#"{"jsonrpc":"2.0","id":1,"method":null,"params":{}}"#, - r#"{"jsonrpc":"2.0","id":1,"result":{},"error":{"code":"bad","message":"error"}}"#, - ] { - assert!( - serde_json::from_str::(json).is_err(), - "{json}" - ); - } - } - - #[test] - fn request_new_sets_version() { - let req = JsonRpcRequest::new(42, "test.method", None); - assert_eq!(req.jsonrpc, "2.0"); - assert_eq!(req.id, 42); - assert_eq!(req.method, "test.method"); - assert!(req.params.is_none()); - } - - #[test] - fn request_serializes_camel_case() { - let req = JsonRpcRequest::new(1, "ping", Some(serde_json::json!({}))); - let json = serde_json::to_string(&req).unwrap(); - assert!(json.contains(r#""jsonrpc":"2.0""#)); - assert!(json.contains(r#""id":1"#)); - assert!(json.contains(r#""method":"ping""#)); - } - - #[test] - fn notification_without_params_omits_field() { - let n = JsonRpcNotification { - jsonrpc: "2.0".into(), - method: "ping".into(), - params: None, - }; - let json = serde_json::to_string(&n).unwrap(); - assert!(!json.contains("params")); - } - - #[test] - fn response_without_error_omits_field() { - let r = JsonRpcResponse { - jsonrpc: "2.0".into(), - id: 1, - result: Some(serde_json::json!(true)), - error: None, - }; - let json = serde_json::to_string(&r).unwrap(); - assert!(!json.contains("error")); - } -} +mod tests; diff --git a/rust/src/jsonrpc/tests.rs b/rust/src/jsonrpc/tests.rs new file mode 100644 index 0000000000..0b7f154c83 --- /dev/null +++ b/rust/src/jsonrpc/tests.rs @@ -0,0 +1,162 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +#![cfg(test)] + +use super::*; + +#[test] +fn listener_negotiation_logs_do_not_echo_remote_errors() { + let message = "request rejected: token=listener-secret-sentinel"; + for method in ["host.getConfiguration", "host.ready"] { + assert_eq!( + remote_error_log_message(method, message), + "listener negotiation request rejected" + ); + } + assert_eq!(remote_error_log_message("ping", message), message); +} + +#[test] +fn deserialize_notification() { + let json = r#"{"jsonrpc":"2.0","method":"session.event","params":{"id":"e1"}}"#; + let msg: JsonRpcMessage = serde_json::from_str(json).unwrap(); + assert!(matches!(msg, JsonRpcMessage::Notification(n) if n.method == "session.event")); +} + +#[test] +fn deserialize_request() { + let json = + r#"{"jsonrpc":"2.0","id":5,"method":"permission.request","params":{"kind":"shell"}}"#; + let msg: JsonRpcMessage = serde_json::from_str(json).unwrap(); + assert!( + matches!(msg, JsonRpcMessage::Request(r) if r.id == 5 && r.method == "permission.request") + ); +} + +#[test] +fn deserialize_response_with_result() { + let json = r#"{"jsonrpc":"2.0","id":3,"result":{"ok":true}}"#; + let msg: JsonRpcMessage = serde_json::from_str(json).unwrap(); + assert!(matches!(msg, JsonRpcMessage::Response(r) if r.id == 3 && !r.is_error())); +} + +#[test] +fn deserialize_error_response() { + let json = r#"{"jsonrpc":"2.0","id":7,"error":{"code":-32600,"message":"Invalid Request","data":{"nested":[1,{"reason":"invalid"}]}}}"#; + let msg: JsonRpcMessage = serde_json::from_str(json).unwrap(); + match msg { + JsonRpcMessage::Response(r) => { + assert!(r.is_error()); + let err = r.error.unwrap(); + assert_eq!(err.code, -32600); + assert_eq!(err.message, "Invalid Request"); + assert_eq!( + err.data, + Some(serde_json::json!({"nested": [1, {"reason": "invalid"}]})) + ); + } + other => panic!("expected Response, got {other:?}"), + } +} + +#[test] +fn deserialize_rejects_non_object() { + let result = serde_json::from_str::(r#""not an object""#); + assert!(result.is_err()); +} + +#[test] +fn deserialize_preserves_optional_payloads() { + for payload in [ + None, + Some(Value::Null), + Some(serde_json::json!(false)), + Some(serde_json::json!(42)), + Some(serde_json::json!("text")), + Some(serde_json::json!([{"nested": [1, null, true]}])), + Some(serde_json::json!({"rows": [{"content": "result"}]})), + ] { + for mut envelope in [ + serde_json::json!({"jsonrpc": "2.0", "method": "notify"}), + serde_json::json!({"jsonrpc": "2.0", "id": 1, "method": "request"}), + serde_json::json!({"jsonrpc": "2.0", "id": 1}), + ] { + let (payload_key, ignored_key) = if envelope.get("method").is_some() { + ("params", "result") + } else { + ("result", "params") + }; + envelope[ignored_key] = serde_json::json!({"ignored": "opposite payload"}); + if let Some(payload) = &payload { + envelope[payload_key] = payload.clone(); + } + let actual = match serde_json::from_value::(envelope).unwrap() { + JsonRpcMessage::Request(request) => request.params, + JsonRpcMessage::Response(response) => response.result, + JsonRpcMessage::Notification(notification) => notification.params, + }; + assert_eq!(actual, payload.clone().filter(|value| !value.is_null())); + } + } +} + +#[test] +fn deserialize_rejects_invalid_metadata() { + for json in [ + r#"{"jsonrpc":null,"method":"notify","params":{"nested":[1]}}"#, + r#"{"jsonrpc":"2.0","method":42,"params":{"nested":[1]}}"#, + r#"{"jsonrpc":"2.0","id":null,"result":{}}"#, + r#"{"jsonrpc":"2.0","id":"1","result":{}}"#, + r#"{"jsonrpc":"2.0","id":-1,"result":{}}"#, + r#"{"jsonrpc":"2.0","id":1,"method":null,"params":{}}"#, + r#"{"jsonrpc":"2.0","id":1,"result":{},"error":{"code":"bad","message":"error"}}"#, + ] { + assert!( + serde_json::from_str::(json).is_err(), + "{json}" + ); + } +} + +#[test] +fn request_new_sets_version() { + let req = JsonRpcRequest::new(42, "test.method", None); + assert_eq!(req.jsonrpc, "2.0"); + assert_eq!(req.id, 42); + assert_eq!(req.method, "test.method"); + assert!(req.params.is_none()); +} + +#[test] +fn request_serializes_camel_case() { + let req = JsonRpcRequest::new(1, "ping", Some(serde_json::json!({}))); + let json = serde_json::to_string(&req).unwrap(); + assert!(json.contains(r#""jsonrpc":"2.0""#)); + assert!(json.contains(r#""id":1"#)); + assert!(json.contains(r#""method":"ping""#)); +} + +#[test] +fn notification_without_params_omits_field() { + let n = JsonRpcNotification { + jsonrpc: "2.0".into(), + method: "ping".into(), + params: None, + }; + let json = serde_json::to_string(&n).unwrap(); + assert!(!json.contains("params")); +} + +#[test] +fn response_without_error_omits_field() { + let r = JsonRpcResponse { + jsonrpc: "2.0".into(), + id: 1, + result: Some(serde_json::json!(true)), + error: None, + }; + let json = serde_json::to_string(&r).unwrap(); + assert!(!json.contains("error")); +} diff --git a/rust/src/lib.rs b/rust/src/lib.rs index 074d9d588f..43040d0944 100644 --- a/rust/src/lib.rs +++ b/rust/src/lib.rs @@ -1269,6 +1269,7 @@ struct ClientInner { models_cache: parking_lot::Mutex>>>, session_fs_configured: bool, session_fs_sqlite_declared: bool, + session_fs_binary_declared: bool, /// Inbound `llmInference.*` dispatcher, installed when /// [`ClientOptions::request_handler`] is set. llm_inference: OnceLock>, @@ -1443,6 +1444,10 @@ impl Client { .as_ref() .and_then(|c| c.capabilities.as_ref()) .is_some_and(|caps| caps.sqlite); + let session_fs_binary_declared = session_fs_config + .as_ref() + .and_then(|c| c.capabilities.as_ref()) + .is_some_and(|caps| caps.binary); let program = match &options.program { CliProgram::Path(path) => { info!(path = %path.display(), "using explicit copilot CLI path"); @@ -1514,6 +1519,7 @@ impl Client { extension_launch_provider.clone(), session_fs_config.is_some(), session_fs_sqlite_declared, + session_fs_binary_declared, options.on_get_trace_context, options.on_github_telemetry, effective_connection_token.clone(), @@ -1549,6 +1555,7 @@ impl Client { extension_launch_provider.clone(), session_fs_config.is_some(), session_fs_sqlite_declared, + session_fs_binary_declared, options.on_get_trace_context, options.on_github_telemetry, effective_connection_token.clone(), @@ -1574,6 +1581,7 @@ impl Client { extension_launch_provider.clone(), session_fs_config.is_some(), session_fs_sqlite_declared, + session_fs_binary_declared, options.on_get_trace_context, options.on_github_telemetry, effective_connection_token.clone(), @@ -1643,6 +1651,7 @@ impl Client { extension_launch_provider.clone(), session_fs_config.is_some(), session_fs_sqlite_declared, + session_fs_binary_declared, options.on_get_trace_context, options.on_github_telemetry, effective_connection_token.clone(), @@ -1698,6 +1707,7 @@ impl Client { let capabilities = cfg.capabilities.as_ref().map(|c| { crate::generated::api_types::SessionFsSetProviderCapabilities { sqlite: Some(c.sqlite), + binary: Some(c.binary), } }); let request = crate::generated::api_types::SessionFsSetProviderRequest { @@ -1821,6 +1831,39 @@ impl Client { None, false, false, + false, + None, + None, + None, + ClientMode::default(), + None, + false, + ) + } + + /// Construct a client with declared session filesystem capabilities for + /// framed transport tests, without starting a CLI process. + #[doc(hidden)] + #[cfg(any(test, feature = "test-support"))] + pub fn from_streams_with_session_fs_config( + reader: impl AsyncRead + Unpin + Send + 'static, + writer: impl AsyncWrite + Unpin + Send + 'static, + cwd: PathBuf, + config: SessionFsConfig, + ) -> Result { + let sqlite = config.capabilities.as_ref().is_some_and(|c| c.sqlite); + let binary = config.capabilities.as_ref().is_some_and(|c| c.binary); + Self::from_transport( + reader, + writer, + None, + None, + cwd, + None, + None, + true, + sqlite, + binary, None, None, None, @@ -1850,6 +1893,7 @@ impl Client { Some(provider), false, false, + false, None, None, None, @@ -1900,6 +1944,7 @@ impl Client { None, false, false, + false, Some(provider), None, None, @@ -1929,6 +1974,7 @@ impl Client { None, false, false, + false, None, None, token, @@ -1958,6 +2004,7 @@ impl Client { None, false, false, + false, None, Some(on_github_telemetry), None, @@ -1998,6 +2045,7 @@ impl Client { None, false, false, + false, None, None, None, @@ -2020,6 +2068,7 @@ impl Client { >, session_fs_configured: bool, session_fs_sqlite_declared: bool, + session_fs_binary_declared: bool, on_get_trace_context: Option>, on_github_telemetry: Option, effective_connection_token: Option, @@ -2075,6 +2124,7 @@ impl Client { models_cache: parking_lot::Mutex::new(Arc::new(tokio::sync::OnceCell::new())), session_fs_configured, session_fs_sqlite_declared, + session_fs_binary_declared, llm_inference: OnceLock::new(), extension_launch_provider: extension_launch_provider.clone(), installation_confirmation: installation_confirmation.clone(), @@ -3257,814 +3307,4 @@ impl Drop for ClientInner { } #[cfg(all(test, feature = "runtime"))] -mod tests { - use super::*; - - #[test] - fn is_transport_failure_matches_request_cancelled() { - let err = Error::from(ErrorKind::Protocol(ProtocolErrorKind::RequestCancelled)); - assert!(err.is_transport_failure()); - } - - #[test] - fn is_transport_failure_matches_io_error() { - let err = Error::from(std::io::Error::new(std::io::ErrorKind::BrokenPipe, "gone")); - assert!(err.is_transport_failure()); - } - - #[test] - fn is_transport_failure_rejects_rpc_error() { - let err = Error::with_message(ErrorKind::Rpc { code: -1 }, "bad"); - assert!(!err.is_transport_failure()); - } - - #[test] - fn is_transport_failure_rejects_session_error() { - let err = Error::from(ErrorKind::Session(SessionErrorKind::NotFound("s1".into()))); - assert!(!err.is_transport_failure()); - } - - #[test] - fn client_options_builder_composes() { - let opts = ClientOptions::new() - .with_program(CliProgram::Path(PathBuf::from("/usr/local/bin/copilot"))) - .with_prefix_args(["node"]) - .with_cwd(PathBuf::from("/tmp")) - .with_env([("KEY", "value")]) - .with_env_remove(["UNWANTED"]) - .with_extra_args(["--quiet"]) - .with_github_token("ghp_test") - .with_use_logged_in_user(false) - .with_log_level(LogLevel::Debug) - .with_session_idle_timeout_seconds(120) - .with_enable_remote_sessions(true); - assert!(matches!(opts.program, CliProgram::Path(_))); - assert_eq!(opts.prefix_args, vec![std::ffi::OsString::from("node")]); - assert_eq!(opts.working_directory, PathBuf::from("/tmp")); - assert_eq!( - opts.env, - vec![( - std::ffi::OsString::from("KEY"), - std::ffi::OsString::from("value") - )] - ); - assert_eq!(opts.env_remove, vec![std::ffi::OsString::from("UNWANTED")]); - assert_eq!(opts.extra_args, vec!["--quiet".to_string()]); - assert_eq!(opts.github_token.as_deref(), Some("ghp_test")); - assert_eq!(opts.use_logged_in_user, Some(false)); - assert!(matches!(opts.log_level, Some(LogLevel::Debug))); - assert_eq!(opts.session_idle_timeout_seconds, Some(120)); - assert!(opts.enable_remote_sessions); - } - - #[test] - fn default_transport_values_resolve_without_process_state() { - assert!(matches!( - resolve_default_transport_value(None).unwrap(), - Transport::Stdio - )); - assert!(matches!( - resolve_default_transport_value(Some("stdio")).unwrap(), - Transport::Stdio - )); - assert!(matches!( - resolve_default_transport_value(Some("INPROCESS")).unwrap(), - Transport::InProcess - )); - assert!(resolve_default_transport_value(Some("tcp")).is_err()); - } - - #[test] - fn inprocess_rejects_process_scoped_options() { - let invalid = [ - ClientOptions::new().with_cwd("."), - ClientOptions::new().with_env([("KEY", "value")]), - ClientOptions::new().with_env_remove(["KEY"]), - ClientOptions::new().with_telemetry(TelemetryConfig::default()), - ClientOptions::new().with_prefix_args(["index.js"]), - ClientOptions::new().with_program(CliProgram::Path("copilot".into())), - ClientOptions::new().with_extra_args(["--verbose"]), - ]; - - for options in invalid { - assert!(validate_inprocess_options(&options).is_err()); - } - } - - #[test] - fn inprocess_allows_typed_runtime_options() { - let options = ClientOptions::new() - .with_base_directory("state") - .with_log_level(LogLevel::Debug) - .with_session_idle_timeout_seconds(10) - .with_github_token("token") - .with_use_logged_in_user(false) - .with_enable_remote_sessions(true); - - assert!(validate_inprocess_options(&options).is_ok()); - } - - #[cfg(not(feature = "in-process"))] - #[tokio::test] - async fn inprocess_requires_cargo_feature() { - let error = Client::start(ClientOptions::new().with_transport(Transport::InProcess)) - .await - .unwrap_err(); - - assert!(error.to_string().contains("in-process")); - } - - #[test] - fn is_transport_failure_rejects_other_protocol_errors() { - let err = Error::from(ErrorKind::Protocol(ProtocolErrorKind::CliStartupTimeout)); - assert!(!err.is_transport_failure()); - } - - #[test] - fn build_command_lets_env_remove_strip_injected_token() { - let opts = ClientOptions { - github_token: Some("secret".to_string()), - env_remove: vec![std::ffi::OsString::from("COPILOT_SDK_AUTH_TOKEN")], - ..Default::default() - }; - let cmd = Client::build_command(Path::new("/bin/echo"), &opts, Path::new("/tmp")); - // get_envs() iter yields the latest action per key — None means removed. - let action = cmd - .as_std() - .get_envs() - .find(|(k, _)| *k == std::ffi::OsStr::new("COPILOT_SDK_AUTH_TOKEN")) - .map(|(_, v)| v); - assert_eq!( - action, - Some(None), - "env_remove should win over github_token" - ); - } - - #[test] - fn build_command_lets_env_override_injected_token() { - let opts = ClientOptions { - github_token: Some("from-options".to_string()), - env: vec![( - std::ffi::OsString::from("COPILOT_SDK_AUTH_TOKEN"), - std::ffi::OsString::from("from-env"), - )], - ..Default::default() - }; - let cmd = Client::build_command(Path::new("/bin/echo"), &opts, Path::new("/tmp")); - let value = cmd - .as_std() - .get_envs() - .find(|(k, _)| *k == std::ffi::OsStr::new("COPILOT_SDK_AUTH_TOKEN")) - .and_then(|(_, v)| v); - assert_eq!(value, Some(std::ffi::OsStr::new("from-env"))); - } - - #[test] - fn build_command_injects_github_token_by_default() { - let opts = ClientOptions { - github_token: Some("just-the-token".to_string()), - ..Default::default() - }; - let cmd = Client::build_command(Path::new("/bin/echo"), &opts, Path::new("/tmp")); - let value = cmd - .as_std() - .get_envs() - .find(|(k, _)| *k == std::ffi::OsStr::new("COPILOT_SDK_AUTH_TOKEN")) - .and_then(|(_, v)| v); - assert_eq!(value, Some(std::ffi::OsStr::new("just-the-token"))); - } - - fn env_value<'a>(cmd: &'a tokio::process::Command, key: &str) -> Option<&'a std::ffi::OsStr> { - cmd.as_std() - .get_envs() - .find(|(k, _)| *k == std::ffi::OsStr::new(key)) - .and_then(|(_, v)| v) - } - - #[test] - fn telemetry_config_builder_composes() { - let cfg = TelemetryConfig::new() - .with_otlp_endpoint("http://collector:4318") - .with_otlp_protocol(OtlpHttpProtocol::HttpProtobuf) - .with_file_path(PathBuf::from("/var/log/copilot.jsonl")) - .with_exporter_type(OtelExporterType::OtlpHttp) - .with_source_name("my-app") - .with_capture_content(true); - - assert_eq!(cfg.otlp_endpoint.as_deref(), Some("http://collector:4318")); - assert_eq!(cfg.otlp_protocol, Some(OtlpHttpProtocol::HttpProtobuf)); - assert_eq!( - cfg.file_path.as_deref(), - Some(Path::new("/var/log/copilot.jsonl")), - ); - assert_eq!(cfg.exporter_type, Some(OtelExporterType::OtlpHttp)); - assert_eq!(cfg.source_name.as_deref(), Some("my-app")); - assert_eq!(cfg.capture_content, Some(true)); - assert!(!cfg.is_empty()); - assert!(TelemetryConfig::new().is_empty()); - } - - #[test] - fn otlp_http_protocol_serde_matches_env_value() { - for (protocol, wire) in [ - (OtlpHttpProtocol::HttpJson, "http/json"), - (OtlpHttpProtocol::HttpProtobuf, "http/protobuf"), - ] { - assert_eq!(protocol.as_str(), wire); - - let serialized = serde_json::to_string(&protocol).unwrap(); - assert_eq!(serialized, format!("\"{wire}\"")); - - let deserialized: OtlpHttpProtocol = serde_json::from_str(&serialized).unwrap(); - assert_eq!(deserialized, protocol); - } - } - - #[test] - fn build_command_sets_otel_env_when_telemetry_enabled() { - let opts = ClientOptions { - telemetry: Some(TelemetryConfig { - otlp_endpoint: Some("http://collector:4318".to_string()), - otlp_protocol: Some(OtlpHttpProtocol::HttpProtobuf), - file_path: Some(PathBuf::from("/var/log/copilot.jsonl")), - exporter_type: Some(OtelExporterType::OtlpHttp), - source_name: Some("my-app".to_string()), - capture_content: Some(true), - }), - ..Default::default() - }; - let cmd = Client::build_command(Path::new("/bin/echo"), &opts, Path::new("/tmp")); - assert_eq!( - env_value(&cmd, "COPILOT_OTEL_ENABLED"), - Some(std::ffi::OsStr::new("true")), - ); - assert_eq!( - env_value(&cmd, "OTEL_EXPORTER_OTLP_ENDPOINT"), - Some(std::ffi::OsStr::new("http://collector:4318")), - ); - assert_eq!( - env_value(&cmd, "OTEL_EXPORTER_OTLP_PROTOCOL"), - Some(std::ffi::OsStr::new("http/protobuf")), - ); - assert_eq!( - env_value(&cmd, "COPILOT_OTEL_FILE_EXPORTER_PATH"), - Some(std::ffi::OsStr::new("/var/log/copilot.jsonl")), - ); - assert_eq!( - env_value(&cmd, "COPILOT_OTEL_EXPORTER_TYPE"), - Some(std::ffi::OsStr::new("otlp-http")), - ); - assert_eq!( - env_value(&cmd, "COPILOT_OTEL_SOURCE_NAME"), - Some(std::ffi::OsStr::new("my-app")), - ); - assert_eq!( - env_value(&cmd, "OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT"), - Some(std::ffi::OsStr::new("true")), - ); - } - - #[test] - fn build_command_omits_otel_env_when_telemetry_none() { - let opts = ClientOptions::default(); - let cmd = Client::build_command(Path::new("/bin/echo"), &opts, Path::new("/tmp")); - for key in [ - "COPILOT_OTEL_ENABLED", - "OTEL_EXPORTER_OTLP_ENDPOINT", - "OTEL_EXPORTER_OTLP_PROTOCOL", - "COPILOT_OTEL_FILE_EXPORTER_PATH", - "COPILOT_OTEL_EXPORTER_TYPE", - "COPILOT_OTEL_SOURCE_NAME", - "OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT", - ] { - assert!( - env_value(&cmd, key).is_none(), - "expected {key} to be unset when telemetry is None", - ); - } - } - - #[test] - fn build_command_omits_unset_telemetry_fields() { - let opts = ClientOptions { - telemetry: Some(TelemetryConfig { - otlp_endpoint: Some("http://collector:4318".to_string()), - ..Default::default() - }), - ..Default::default() - }; - let cmd = Client::build_command(Path::new("/bin/echo"), &opts, Path::new("/tmp")); - // The one set field plus the implicit enabled flag should propagate. - assert_eq!( - env_value(&cmd, "COPILOT_OTEL_ENABLED"), - Some(std::ffi::OsStr::new("true")), - ); - assert_eq!( - env_value(&cmd, "OTEL_EXPORTER_OTLP_ENDPOINT"), - Some(std::ffi::OsStr::new("http://collector:4318")), - ); - // None of the other fields should leak as env vars. - for key in [ - "OTEL_EXPORTER_OTLP_PROTOCOL", - "COPILOT_OTEL_FILE_EXPORTER_PATH", - "COPILOT_OTEL_EXPORTER_TYPE", - "COPILOT_OTEL_SOURCE_NAME", - "OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT", - ] { - assert!(env_value(&cmd, key).is_none(), "{key} should be unset"); - } - } - - #[test] - fn build_command_lets_user_env_override_telemetry() { - let opts = ClientOptions { - telemetry: Some(TelemetryConfig { - otlp_endpoint: Some("http://from-config:4318".to_string()), - ..Default::default() - }), - env: vec![( - std::ffi::OsString::from("OTEL_EXPORTER_OTLP_ENDPOINT"), - std::ffi::OsString::from("http://from-user-env:4318"), - )], - ..Default::default() - }; - let cmd = Client::build_command(Path::new("/bin/echo"), &opts, Path::new("/tmp")); - assert_eq!( - env_value(&cmd, "OTEL_EXPORTER_OTLP_ENDPOINT"), - Some(std::ffi::OsStr::new("http://from-user-env:4318")), - "user-supplied options.env should override telemetry config", - ); - } - - #[test] - fn build_command_sets_copilot_home_env_when_configured() { - let opts = ClientOptions::new().with_base_directory(PathBuf::from("/custom/copilot")); - let cmd = Client::build_command(Path::new("/bin/echo"), &opts, Path::new("/tmp")); - assert_eq!( - env_value(&cmd, "COPILOT_HOME"), - Some(std::ffi::OsStr::new("/custom/copilot")), - ); - - let opts = ClientOptions::default(); - let cmd = Client::build_command(Path::new("/bin/echo"), &opts, Path::new("/tmp")); - assert!(env_value(&cmd, "COPILOT_HOME").is_none()); - } - - #[test] - fn build_command_sets_connection_token_env_when_configured() { - let opts = ClientOptions::new().with_transport(Transport::Tcp { - port: 0, - connection_token: Some("secret-token".to_string()), - }); - let cmd = Client::build_command(Path::new("/bin/echo"), &opts, Path::new("/tmp")); - assert_eq!( - env_value(&cmd, "COPILOT_CONNECTION_TOKEN"), - Some(std::ffi::OsStr::new("secret-token")), - ); - - let opts = ClientOptions::default(); - let cmd = Client::build_command(Path::new("/bin/echo"), &opts, Path::new("/tmp")); - assert!(env_value(&cmd, "COPILOT_CONNECTION_TOKEN").is_none()); - } - - #[tokio::test] - async fn start_rejects_empty_connection_token() { - let opts = ClientOptions::new() - .with_transport(Transport::Tcp { - port: 0, - connection_token: Some(String::new()), - }) - .with_program(CliProgram::Path(PathBuf::from("/bin/echo"))); - let err = Client::start(opts).await.unwrap_err(); - assert!( - matches!(err.kind(), ErrorKind::InvalidConfig), - "got {err:?}" - ); - } - - #[tokio::test] - async fn start_rejects_empty_external_connection_token() { - let opts = ClientOptions::new() - .with_transport(Transport::External { - host: "127.0.0.1".to_string(), - port: 1, - connection_token: Some(String::new()), - }) - .with_program(CliProgram::Path(PathBuf::from("/bin/echo"))); - let err = Client::start(opts).await.unwrap_err(); - assert!( - matches!(err.kind(), ErrorKind::InvalidConfig), - "got {err:?}" - ); - } - - #[test] - fn telemetry_config_capture_content_serializes_as_lowercase_bool() { - let opts_true = ClientOptions { - telemetry: Some(TelemetryConfig { - capture_content: Some(true), - ..Default::default() - }), - ..Default::default() - }; - let opts_false = ClientOptions { - telemetry: Some(TelemetryConfig { - capture_content: Some(false), - ..Default::default() - }), - ..Default::default() - }; - let cmd_true = Client::build_command(Path::new("/bin/echo"), &opts_true, Path::new("/tmp")); - let cmd_false = - Client::build_command(Path::new("/bin/echo"), &opts_false, Path::new("/tmp")); - assert_eq!( - env_value( - &cmd_true, - "OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT" - ), - Some(std::ffi::OsStr::new("true")), - ); - assert_eq!( - env_value( - &cmd_false, - "OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT" - ), - Some(std::ffi::OsStr::new("false")), - ); - } - - #[test] - fn session_idle_timeout_args_are_omitted_by_default() { - let opts = ClientOptions::default(); - assert!(Client::session_idle_timeout_args(&opts).is_empty()); - } - - #[test] - fn session_idle_timeout_args_omitted_for_zero() { - let opts = ClientOptions { - session_idle_timeout_seconds: Some(0), - ..Default::default() - }; - assert!(Client::session_idle_timeout_args(&opts).is_empty()); - } - - #[test] - fn session_idle_timeout_args_emit_flag_for_positive_value() { - let opts = ClientOptions { - session_idle_timeout_seconds: Some(300), - ..Default::default() - }; - assert_eq!( - Client::session_idle_timeout_args(&opts), - vec!["--session-idle-timeout".to_string(), "300".to_string()] - ); - } - - #[test] - fn remote_args_omitted_by_default() { - let opts = ClientOptions::default(); - assert!(Client::remote_args(&opts).is_empty()); - } - - #[test] - fn remote_args_emit_flag_when_enabled() { - let opts = ClientOptions { - enable_remote_sessions: true, - ..Default::default() - }; - assert_eq!(Client::remote_args(&opts), vec!["--remote".to_string()]); - } - - #[test] - fn log_level_args_omitted_when_unset() { - let opts = ClientOptions::default(); - assert!(opts.log_level.is_none()); - assert!( - Client::log_level_args(&opts).is_empty(), - "with no caller-supplied log_level the SDK must not pass --log-level" - ); - } - - #[test] - fn log_level_args_emit_flag_when_set() { - let opts = ClientOptions::default().with_log_level(LogLevel::Debug); - assert_eq!(Client::log_level_args(&opts), vec!["--log-level", "debug"]); - } - - #[test] - fn cli_mode_opts_into_process_logging_without_changing_empty_mode_environment() { - for (mode, expected) in [ - (ClientMode::Empty, None), - (ClientMode::CopilotCli, Some("1")), - ] { - let mut options = ClientOptions::default().with_mode(mode); - options.env.push(( - std::ffi::OsString::from("COPILOT_RUNTIME_PROCESS_FILE_LOGGING"), - std::ffi::OsString::from("opposite"), - )); - let command = - Client::build_command(Path::new("copilot-runtime"), &options, Path::new(".")); - let actual = command - .as_std() - .get_envs() - .find(|(key, _)| { - *key == std::ffi::OsStr::new("COPILOT_RUNTIME_PROCESS_FILE_LOGGING") - }) - .and_then(|(_, value)| value); - assert_eq!( - actual, - Some(std::ffi::OsStr::new(expected.unwrap_or("opposite"))), - "mode: {mode:?}" - ); - } - } - - #[test] - fn log_level_str_round_trips() { - for level in [ - LogLevel::None, - LogLevel::Error, - LogLevel::Warning, - LogLevel::Info, - LogLevel::Debug, - LogLevel::All, - ] { - let s = level.as_str(); - let json = serde_json::to_string(&level).unwrap(); - assert_eq!(json, format!("\"{s}\"")); - let parsed: LogLevel = serde_json::from_str(&json).unwrap(); - assert_eq!(parsed, level); - } - } - - #[test] - fn client_options_debug_redacts_handler() { - struct StubHandler; - #[async_trait] - impl ListModelsHandler for StubHandler { - async fn list_models(&self) -> Result> { - Ok(vec![]) - } - } - let opts = ClientOptions { - on_list_models: Some(Arc::new(StubHandler)), - github_token: Some("secret-token".into()), - ..Default::default() - }; - let debug = format!("{opts:?}"); - assert!(debug.contains("on_list_models: Some(\"\")")); - assert!(debug.contains("github_token: Some(\"\")")); - assert!(!debug.contains("secret-token")); - } - - #[tokio::test] - async fn list_models_uses_on_list_models_handler_when_set() { - use std::sync::atomic::{AtomicUsize, Ordering}; - - struct CountingHandler { - calls: Arc, - models: Vec, - } - #[async_trait] - impl ListModelsHandler for CountingHandler { - async fn list_models(&self) -> Result> { - self.calls.fetch_add(1, Ordering::SeqCst); - Ok(self.models.clone()) - } - } - - let calls = Arc::new(AtomicUsize::new(0)); - let model = Model { - id: "byok-gpt-4".into(), - name: "BYOK GPT-4".into(), - ..Default::default() - }; - let handler: Arc = Arc::new(CountingHandler { - calls: Arc::clone(&calls), - models: vec![model.clone()], - }); - - let client = client_with_list_models_handler(handler); - - let result = client.list_models().await.unwrap(); - assert_eq!(result.len(), 1); - assert_eq!(result[0].id, "byok-gpt-4"); - assert_eq!(calls.load(Ordering::SeqCst), 1); - } - - #[tokio::test] - async fn list_models_serializes_concurrent_cache_misses() { - use std::sync::atomic::{AtomicUsize, Ordering}; - - struct SlowCountingHandler { - calls: Arc, - models: Vec, - } - #[async_trait] - impl ListModelsHandler for SlowCountingHandler { - async fn list_models(&self) -> Result> { - self.calls.fetch_add(1, Ordering::SeqCst); - tokio::time::sleep(std::time::Duration::from_millis(25)).await; - Ok(self.models.clone()) - } - } - - let calls = Arc::new(AtomicUsize::new(0)); - let model = Model { - id: "single-flight-model".into(), - name: "Single Flight Model".into(), - ..Default::default() - }; - let handler: Arc = Arc::new(SlowCountingHandler { - calls: Arc::clone(&calls), - models: vec![model], - }); - let client = client_with_list_models_handler(handler); - - let (first, second) = tokio::join!(client.list_models(), client.list_models()); - assert_eq!(first.unwrap()[0].id, "single-flight-model"); - assert_eq!(second.unwrap()[0].id, "single-flight-model"); - assert_eq!(calls.load(Ordering::SeqCst), 1); - } - - #[tokio::test] - async fn cancelled_resume_session_unregisters_pending_session() { - let (client_write, _server_read) = tokio::io::duplex(8192); - let (_server_write, client_read) = tokio::io::duplex(8192); - let client = Client::from_streams(client_read, client_write, std::env::temp_dir()).unwrap(); - assert!(client.startup_timings().is_none()); - let session_id = SessionId::new("resume-cancel-test"); - let handle = tokio::spawn({ - let client = client.clone(); - async move { - client - .resume_session(ResumeSessionConfig::new(session_id)) - .await - } - }); - - wait_for_pending_session_registration(&client).await; - handle.abort(); - let _ = handle.await; - - assert!(client.inner.router.session_ids().is_empty()); - client.force_stop(); - } - - #[cfg(any(unix, windows))] - #[tokio::test] - async fn dropping_last_client_kills_spawned_cli() { - let temp = tempfile::tempdir().unwrap(); - let ready = temp.path().join("ready"); - let survived = temp.path().join("survived"); - let child = test_child_command(temp.path(), &ready, &survived) - .spawn() - .unwrap(); - let (client_write, _server_read) = tokio::io::duplex(64); - let (_server_write, client_read) = tokio::io::duplex(64); - let client = Client::from_transport( - client_read, - client_write, - Some(child), - None, - temp.path().to_path_buf(), - None, - None, - false, - false, - None, - None, - None, - ClientMode::default(), - None, - false, - ) - .unwrap(); - - wait_for_test_child(&ready).await; - drop(client); - - assert_test_child_killed(&survived).await; - } - - #[cfg(any(unix, windows))] - #[tokio::test] - async fn spawned_child_is_killed_when_dropped() { - let temp = tempfile::tempdir().unwrap(); - let ready = temp.path().join("ready"); - let survived = temp.path().join("survived"); - let child = test_child_command(temp.path(), &ready, &survived) - .spawn() - .unwrap(); - - wait_for_test_child(&ready).await; - drop(child); - - assert_test_child_killed(&survived).await; - } - - #[cfg(any(unix, windows))] - fn test_child_command(temp: &Path, ready: &Path, survived: &Path) -> Command { - let mut command = Client::build_command(Path::new("node"), &ClientOptions::default(), temp); - #[cfg(windows)] - { - const CREATE_NO_WINDOW: u32 = 0x0800_0000; - command.creation_flags(CREATE_NO_WINDOW); - } - command - .args([ - "-e", - r#" - const fs = require("node:fs"); - fs.writeFileSync(process.env.READY, "ready"); - setTimeout(() => fs.writeFileSync(process.env.SURVIVED, "survived"), 1000); - "#, - ]) - .env("READY", ready) - .env("SURVIVED", survived) - .stderr(Stdio::inherit()); - command - } - - #[cfg(any(unix, windows))] - async fn wait_for_test_child(ready: &Path) { - let deadline = tokio::time::Instant::now() + Duration::from_secs(30); - while !ready.exists() { - assert!( - tokio::time::Instant::now() < deadline, - "child did not report readiness" - ); - tokio::time::sleep(Duration::from_millis(10)).await; - } - } - - #[cfg(any(unix, windows))] - async fn assert_test_child_killed(survived: &Path) { - tokio::time::sleep(Duration::from_millis(1500)).await; - - assert!( - !survived.exists(), - "child survived after its owner was dropped" - ); - } - - fn client_with_list_models_handler(handler: Arc) -> Client { - Client { - ahp_host_sessions: None, - inner: Arc::new(ClientInner { - ahp_host_callbacks: Arc::new(ahp_host::ExitCallbacks::default()), - ahp_host_sessions: std::sync::Weak::new(), - child: parking_lot::Mutex::new(None), - owns_stdio: false, - force_stop_requested: tokio_util::sync::CancellationToken::new(), - process_tree: parking_lot::Mutex::new(None), - #[cfg(feature = "in-process")] - ffi_host: parking_lot::Mutex::new(None), - rpc: { - let (req_tx, _req_rx) = mpsc::unbounded_channel(); - let (notif_tx, _notif_rx) = broadcast::channel(16); - let (read_pipe, _write_pipe) = tokio::io::duplex(64); - let (_unused_read, write_pipe) = tokio::io::duplex(64); - JsonRpcClient::new(write_pipe, read_pipe, notif_tx, req_tx) - }, - cwd: PathBuf::from("."), - request_rx: parking_lot::Mutex::new(None), - notification_tx: broadcast::channel(16).0, - router: router::SessionRouter::new(), - github_token_registry: Arc::new(github_token::GitHubTokenRegistry::new()), - negotiated_protocol_version: OnceLock::new(), - state: parking_lot::Mutex::new(ConnectionState::Connected), - lifecycle_tx: broadcast::channel(16).0, - on_list_models: Some(handler), - models_cache: parking_lot::Mutex::new(Arc::new(tokio::sync::OnceCell::new())), - session_fs_configured: false, - session_fs_sqlite_declared: false, - llm_inference: OnceLock::new(), - extension_launch_provider: Arc::new( - extension_launch_provider::ExtensionLaunchProviderDispatcher::new(None), - ), - installation_confirmation: Arc::new( - installation_confirmation::InstallationConfirmationDispatcher::new(), - ), - on_github_telemetry: None, - on_get_trace_context: None, - effective_connection_token: None, - mode: ClientMode::default(), - client_info: None, - startup_timings: OnceLock::new(), - }), - } - } - - async fn wait_for_pending_session_registration(client: &Client) { - let deadline = tokio::time::Instant::now() + std::time::Duration::from_secs(1); - while client.inner.router.session_ids().is_empty() { - assert!( - tokio::time::Instant::now() < deadline, - "session was not registered" - ); - tokio::time::sleep(std::time::Duration::from_millis(10)).await; - } - } -} +mod tests; diff --git a/rust/src/mode.rs b/rust/src/mode.rs index 2b1ab897ce..671b32ec7d 100644 --- a/rust/src/mode.rs +++ b/rust/src/mode.rs @@ -301,270 +301,4 @@ pub(crate) fn experimental_mode_for_mode(mode: ClientMode, supplied: Option = ToolSet::new().add_mcp("*").unwrap().into(); - assert_eq!(v, vec!["mcp:*"]); - } - - #[test] - fn validate_tool_filter_list_rejects_bare_star() { - let bad = vec!["*".to_string()]; - assert!(validate_tool_filter_list("availableTools", Some(&bad)).is_err()); - } - - #[test] - fn validate_tool_filter_list_allows_qualified_star() { - let ok = vec!["builtin:*".to_string(), "mcp:*".to_string()]; - assert!(validate_tool_filter_list("availableTools", Some(&ok)).is_ok()); - } - - #[test] - fn validate_tool_filter_list_none_is_ok() { - assert!(validate_tool_filter_list("availableTools", None).is_ok()); - } - - #[test] - fn builtin_tools_isolated_contents() { - assert!(BUILTIN_TOOLS_ISOLATED.contains(&"ask_user")); - assert!(BUILTIN_TOOLS_ISOLATED.contains(&"task_complete")); - assert!(BUILTIN_TOOLS_ISOLATED.contains(&"skill")); - assert!(!BUILTIN_TOOLS_ISOLATED.contains(&"bash")); - assert!(!BUILTIN_TOOLS_ISOLATED.contains(&"edit")); - assert!(!BUILTIN_TOOLS_ISOLATED.contains(&"web_fetch")); - } - - #[test] - fn client_mode_default_is_copilot_cli() { - assert_eq!(ClientMode::default(), ClientMode::CopilotCli); - } - - #[test] - fn system_message_copilot_cli_passes_through_unchanged() { - let cfg = SystemMessageConfig { - mode: Some("append".to_string()), - content: Some("hello".to_string()), - sections: None, - }; - let out = system_message_for_mode(ClientMode::CopilotCli, Some(cfg.clone())); - let out = out.unwrap(); - assert_eq!(out.mode.as_deref(), Some("append")); - assert_eq!(out.content.as_deref(), Some("hello")); - } - - #[test] - fn system_message_empty_none_injects_strip() { - let out = system_message_for_mode(ClientMode::Empty, None).unwrap(); - assert_eq!(out.mode.as_deref(), Some("customize")); - let sections = out.sections.unwrap(); - let env = sections.get("environment_context").unwrap(); - assert_eq!(env.action.as_deref(), Some("remove")); - } - - #[test] - fn system_message_empty_append_promoted_to_customize() { - let cfg = SystemMessageConfig { - mode: Some("append".to_string()), - content: Some("hi".to_string()), - sections: None, - }; - let out = system_message_for_mode(ClientMode::Empty, Some(cfg)).unwrap(); - assert_eq!(out.mode.as_deref(), Some("customize")); - assert_eq!(out.content.as_deref(), Some("hi")); - let sections = out.sections.unwrap(); - assert!(sections.contains_key("environment_context")); - } - - #[test] - fn system_message_empty_replace_passes_through() { - let cfg = SystemMessageConfig { - mode: Some("replace".to_string()), - content: Some("verbatim".to_string()), - sections: None, - }; - let out = system_message_for_mode(ClientMode::Empty, Some(cfg.clone())).unwrap(); - assert_eq!(out.mode.as_deref(), Some("replace")); - assert_eq!(out.content.as_deref(), Some("verbatim")); - assert!(out.sections.is_none()); - } - - #[test] - fn system_message_empty_customize_with_env_context_preserved() { - let mut sections = HashMap::new(); - sections.insert( - "environment_context".to_string(), - SectionOverride { - action: Some("replace".to_string()), - content: Some("custom env".to_string()), - }, - ); - let cfg = SystemMessageConfig { - mode: Some("customize".to_string()), - content: None, - sections: Some(sections), - }; - let out = system_message_for_mode(ClientMode::Empty, Some(cfg)).unwrap(); - let env = out.sections.unwrap().remove("environment_context").unwrap(); - assert_eq!(env.action.as_deref(), Some("replace")); - assert_eq!(env.content.as_deref(), Some("custom env")); - } - - #[test] - fn system_message_empty_customize_without_env_context_gets_strip() { - let mut sections = HashMap::new(); - sections.insert( - "other_section".to_string(), - SectionOverride { - action: Some("replace".to_string()), - content: Some("body".to_string()), - }, - ); - let cfg = SystemMessageConfig { - mode: Some("customize".to_string()), - content: None, - sections: Some(sections), - }; - let out = system_message_for_mode(ClientMode::Empty, Some(cfg)).unwrap(); - let secs = out.sections.unwrap(); - assert!(secs.contains_key("other_section")); - let env = secs.get("environment_context").unwrap(); - assert_eq!(env.action.as_deref(), Some("remove")); - } - - #[test] - fn memory_copilot_cli_leaves_unset_when_not_supplied() { - assert_eq!(memory_for_mode(ClientMode::CopilotCli, None), None); - } - - #[test] - fn memory_copilot_cli_preserves_supplied() { - assert_eq!( - memory_for_mode(ClientMode::CopilotCli, Some(MemoryConfiguration::enabled())), - Some(MemoryConfiguration::enabled()) - ); - } - - #[test] - fn memory_empty_defaults_to_disabled() { - assert_eq!( - memory_for_mode(ClientMode::Empty, None), - Some(MemoryConfiguration::disabled()) - ); - } - - #[test] - fn memory_empty_preserves_supplied() { - assert_eq!( - memory_for_mode(ClientMode::Empty, Some(MemoryConfiguration::enabled())), - Some(MemoryConfiguration::enabled()) - ); - } - - #[test] - fn experimental_mode_defaults_false_in_empty_mode() { - assert_eq!( - experimental_mode_for_mode(ClientMode::Empty, None), - Some(false) - ); - assert_eq!( - experimental_mode_for_mode(ClientMode::Empty, Some(true)), - Some(true) - ); - assert_eq!( - experimental_mode_for_mode(ClientMode::Empty, Some(false)), - Some(false) - ); - } - - #[test] - fn experimental_mode_remains_runtime_controlled_in_copilot_cli_mode() { - assert_eq!( - experimental_mode_for_mode(ClientMode::CopilotCli, None), - None - ); - } -} +mod tests; diff --git a/rust/src/mode/tests.rs b/rust/src/mode/tests.rs new file mode 100644 index 0000000000..4c486fd7bf --- /dev/null +++ b/rust/src/mode/tests.rs @@ -0,0 +1,271 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +#![cfg(test)] + +use super::*; + +#[test] +fn custom_agents_local_only_respects_mode_and_caller_value() { + assert_eq!( + resolve_custom_agents_local_only(ClientMode::Empty, None), + Some(true) + ); + assert_eq!( + resolve_custom_agents_local_only(ClientMode::Empty, Some(false)), + Some(false) + ); + assert_eq!( + resolve_custom_agents_local_only(ClientMode::CopilotCli, None), + None + ); +} + +#[test] +fn tool_set_emits_source_qualified_patterns() { + let v = ToolSet::new() + .add_builtin("bash") + .unwrap() + .add_builtin("*") + .unwrap() + .add_custom("foo") + .unwrap() + .add_custom("*") + .unwrap() + .add_mcp("github-list_issues") + .unwrap() + .add_mcp("*") + .unwrap() + .to_vec(); + assert_eq!( + v, + vec![ + "builtin:bash", + "builtin:*", + "custom:foo", + "custom:*", + "mcp:github-list_issues", + "mcp:*", + ] + ); +} + +#[test] +fn tool_set_add_builtin_many() { + let v = ToolSet::new() + .add_builtin_many(BUILTIN_TOOLS_ISOLATED) + .unwrap() + .into_vec(); + assert_eq!(v.len(), BUILTIN_TOOLS_ISOLATED.len()); + assert_eq!(v[0], format!("builtin:{}", BUILTIN_TOOLS_ISOLATED[0])); +} + +#[test] +fn tool_set_rejects_invalid_names() { + for bad in ["bash!", "with space", "colon:name", "", "wild*card"] { + assert!( + ToolSet::new().add_builtin(bad).is_err(), + "expected '{bad}' to be rejected" + ); + assert!(ToolSet::new().add_custom(bad).is_err()); + assert!(ToolSet::new().add_mcp(bad).is_err()); + } +} + +#[test] +fn tool_set_accepts_wildcard_and_underscores_and_dashes() { + assert!(ToolSet::new().add_builtin("*").is_ok()); + assert!(ToolSet::new().add_mcp("github-list_issues").is_ok()); + assert!(ToolSet::new().add_custom("A_b-9").is_ok()); +} + +#[test] +fn into_vec_is_idempotent_with_to_vec() { + let ts = ToolSet::new().add_builtin("bash").unwrap(); + assert_eq!(ts.to_vec(), vec!["builtin:bash"]); + assert_eq!(ts.into_vec(), vec!["builtin:bash"]); +} + +#[test] +fn into_vec_string_conversion() { + let v: Vec = ToolSet::new().add_mcp("*").unwrap().into(); + assert_eq!(v, vec!["mcp:*"]); +} + +#[test] +fn validate_tool_filter_list_rejects_bare_star() { + let bad = vec!["*".to_string()]; + assert!(validate_tool_filter_list("availableTools", Some(&bad)).is_err()); +} + +#[test] +fn validate_tool_filter_list_allows_qualified_star() { + let ok = vec!["builtin:*".to_string(), "mcp:*".to_string()]; + assert!(validate_tool_filter_list("availableTools", Some(&ok)).is_ok()); +} + +#[test] +fn validate_tool_filter_list_none_is_ok() { + assert!(validate_tool_filter_list("availableTools", None).is_ok()); +} + +#[test] +fn builtin_tools_isolated_contents() { + assert!(BUILTIN_TOOLS_ISOLATED.contains(&"ask_user")); + assert!(BUILTIN_TOOLS_ISOLATED.contains(&"task_complete")); + assert!(BUILTIN_TOOLS_ISOLATED.contains(&"skill")); + assert!(!BUILTIN_TOOLS_ISOLATED.contains(&"bash")); + assert!(!BUILTIN_TOOLS_ISOLATED.contains(&"edit")); + assert!(!BUILTIN_TOOLS_ISOLATED.contains(&"web_fetch")); +} + +#[test] +fn client_mode_default_is_copilot_cli() { + assert_eq!(ClientMode::default(), ClientMode::CopilotCli); +} + +#[test] +fn system_message_copilot_cli_passes_through_unchanged() { + let cfg = SystemMessageConfig { + mode: Some("append".to_string()), + content: Some("hello".to_string()), + sections: None, + }; + let out = system_message_for_mode(ClientMode::CopilotCli, Some(cfg.clone())); + let out = out.unwrap(); + assert_eq!(out.mode.as_deref(), Some("append")); + assert_eq!(out.content.as_deref(), Some("hello")); +} + +#[test] +fn system_message_empty_none_injects_strip() { + let out = system_message_for_mode(ClientMode::Empty, None).unwrap(); + assert_eq!(out.mode.as_deref(), Some("customize")); + let sections = out.sections.unwrap(); + let env = sections.get("environment_context").unwrap(); + assert_eq!(env.action.as_deref(), Some("remove")); +} + +#[test] +fn system_message_empty_append_promoted_to_customize() { + let cfg = SystemMessageConfig { + mode: Some("append".to_string()), + content: Some("hi".to_string()), + sections: None, + }; + let out = system_message_for_mode(ClientMode::Empty, Some(cfg)).unwrap(); + assert_eq!(out.mode.as_deref(), Some("customize")); + assert_eq!(out.content.as_deref(), Some("hi")); + let sections = out.sections.unwrap(); + assert!(sections.contains_key("environment_context")); +} + +#[test] +fn system_message_empty_replace_passes_through() { + let cfg = SystemMessageConfig { + mode: Some("replace".to_string()), + content: Some("verbatim".to_string()), + sections: None, + }; + let out = system_message_for_mode(ClientMode::Empty, Some(cfg.clone())).unwrap(); + assert_eq!(out.mode.as_deref(), Some("replace")); + assert_eq!(out.content.as_deref(), Some("verbatim")); + assert!(out.sections.is_none()); +} + +#[test] +fn system_message_empty_customize_with_env_context_preserved() { + let mut sections = HashMap::new(); + sections.insert( + "environment_context".to_string(), + SectionOverride { + action: Some("replace".to_string()), + content: Some("custom env".to_string()), + }, + ); + let cfg = SystemMessageConfig { + mode: Some("customize".to_string()), + content: None, + sections: Some(sections), + }; + let out = system_message_for_mode(ClientMode::Empty, Some(cfg)).unwrap(); + let env = out.sections.unwrap().remove("environment_context").unwrap(); + assert_eq!(env.action.as_deref(), Some("replace")); + assert_eq!(env.content.as_deref(), Some("custom env")); +} + +#[test] +fn system_message_empty_customize_without_env_context_gets_strip() { + let mut sections = HashMap::new(); + sections.insert( + "other_section".to_string(), + SectionOverride { + action: Some("replace".to_string()), + content: Some("body".to_string()), + }, + ); + let cfg = SystemMessageConfig { + mode: Some("customize".to_string()), + content: None, + sections: Some(sections), + }; + let out = system_message_for_mode(ClientMode::Empty, Some(cfg)).unwrap(); + let secs = out.sections.unwrap(); + assert!(secs.contains_key("other_section")); + let env = secs.get("environment_context").unwrap(); + assert_eq!(env.action.as_deref(), Some("remove")); +} + +#[test] +fn memory_copilot_cli_leaves_unset_when_not_supplied() { + assert_eq!(memory_for_mode(ClientMode::CopilotCli, None), None); +} + +#[test] +fn memory_copilot_cli_preserves_supplied() { + assert_eq!( + memory_for_mode(ClientMode::CopilotCli, Some(MemoryConfiguration::enabled())), + Some(MemoryConfiguration::enabled()) + ); +} + +#[test] +fn memory_empty_defaults_to_disabled() { + assert_eq!( + memory_for_mode(ClientMode::Empty, None), + Some(MemoryConfiguration::disabled()) + ); +} + +#[test] +fn memory_empty_preserves_supplied() { + assert_eq!( + memory_for_mode(ClientMode::Empty, Some(MemoryConfiguration::enabled())), + Some(MemoryConfiguration::enabled()) + ); +} + +#[test] +fn experimental_mode_defaults_false_in_empty_mode() { + assert_eq!( + experimental_mode_for_mode(ClientMode::Empty, None), + Some(false) + ); + assert_eq!( + experimental_mode_for_mode(ClientMode::Empty, Some(true)), + Some(true) + ); + assert_eq!( + experimental_mode_for_mode(ClientMode::Empty, Some(false)), + Some(false) + ); +} + +#[test] +fn experimental_mode_remains_runtime_controlled_in_copilot_cli_mode() { + assert_eq!( + experimental_mode_for_mode(ClientMode::CopilotCli, None), + None + ); +} diff --git a/rust/src/permission.rs b/rust/src/permission.rs index 57c0785708..6fe4f6bc94 100644 --- a/rust/src/permission.rs +++ b/rust/src/permission.rs @@ -135,153 +135,4 @@ impl PermissionHandler for PolicyHandler { } #[cfg(test)] -mod tests { - use super::*; - - fn data() -> PermissionRequestData { - PermissionRequestData { - extra: serde_json::json!({ "tool": "shell" }), - ..Default::default() - } - } - - #[tokio::test] - async fn approve_all_approves() { - let h = approve_all(); - assert!(matches!( - h.handle(SessionId::from("s"), RequestId::new("1"), data()) - .await, - PermissionResult::Decision { - decision: crate::types::PermissionDecision::ApproveOnce(_), - .. - } - )); - } - - #[tokio::test] - async fn approve_all_fails_when_managed_settings_enabled() { - let h = approve_all(); - let mut request = data(); - request.managed_settings_enabled = true; - assert!(matches!( - h.handle(SessionId::from("s"), RequestId::new("1"), request) - .await, - PermissionResult::Decision { - decision: crate::types::PermissionDecision::UserNotAvailable(_), - .. - } - )); - } - - #[tokio::test] - async fn deny_all_denies() { - let h = deny_all(); - assert!(matches!( - h.handle(SessionId::from("s"), RequestId::new("1"), data()) - .await, - PermissionResult::Decision { - decision: crate::types::PermissionDecision::Reject(_), - .. - } - )); - } - - #[tokio::test] - async fn approve_if_consults_predicate() { - let h = approve_if(|d| d.extra.get("tool").and_then(|v| v.as_str()) != Some("shell")); - assert!(matches!( - h.handle(SessionId::from("s"), RequestId::new("1"), data()) - .await, - PermissionResult::Decision { - decision: crate::types::PermissionDecision::Reject(_), - .. - } - )); - } - - #[tokio::test] - async fn approve_if_leaves_managed_approval_pending_when_predicate_approves() { - let h = approve_if(|_| true); - let mut request = data(); - request.managed_approval_required = Some(true); - assert!(matches!( - h.handle(SessionId::from("s"), RequestId::new("1"), request) - .await, - PermissionResult::NoResult - )); - } - - #[tokio::test] - async fn approve_if_still_rejects_managed_request_when_predicate_denies() { - let h = approve_if(|_| false); - let mut request = data(); - request.managed_approval_required = Some(true); - assert!(matches!( - h.handle(SessionId::from("s"), RequestId::new("1"), request) - .await, - PermissionResult::Decision { - decision: crate::types::PermissionDecision::Reject(_), - .. - } - )); - } - - #[tokio::test] - async fn resolve_handler_policy_wins() { - struct AlwaysApprove; - #[async_trait] - impl PermissionHandler for AlwaysApprove { - async fn handle( - &self, - _: SessionId, - _: RequestId, - _: PermissionRequestData, - ) -> PermissionResult { - PermissionResult::approve_once() - } - } - let resolved = - resolve_handler(Some(Arc::new(AlwaysApprove)), Some(Policy::DenyAll)).unwrap(); - // Policy wins -- the AlwaysApprove handler is discarded. - assert!(matches!( - resolved - .handle(SessionId::from("s"), RequestId::new("1"), data()) - .await, - PermissionResult::Decision { - decision: crate::types::PermissionDecision::Reject(_), - .. - } - )); - } - - #[tokio::test] - async fn resolve_handler_with_only_handler() { - struct H; - #[async_trait] - impl PermissionHandler for H { - async fn handle( - &self, - _: SessionId, - _: RequestId, - _: PermissionRequestData, - ) -> PermissionResult { - PermissionResult::approve_once() - } - } - let resolved = resolve_handler(Some(Arc::new(H)), None).unwrap(); - assert!(matches!( - resolved - .handle(SessionId::from("s"), RequestId::new("1"), data()) - .await, - PermissionResult::Decision { - decision: crate::types::PermissionDecision::ApproveOnce(_), - .. - } - )); - } - - #[test] - fn resolve_handler_with_neither_returns_none() { - assert!(resolve_handler(None, None).is_none()); - } -} +mod tests; diff --git a/rust/src/permission/tests.rs b/rust/src/permission/tests.rs new file mode 100644 index 0000000000..0a4afab545 --- /dev/null +++ b/rust/src/permission/tests.rs @@ -0,0 +1,153 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +#![cfg(test)] + +use super::*; + +fn data() -> PermissionRequestData { + PermissionRequestData { + extra: serde_json::json!({ "tool": "shell" }), + ..Default::default() + } +} + +#[tokio::test] +async fn approve_all_approves() { + let h = approve_all(); + assert!(matches!( + h.handle(SessionId::from("s"), RequestId::new("1"), data()) + .await, + PermissionResult::Decision { + decision: crate::types::PermissionDecision::ApproveOnce(_), + .. + } + )); +} + +#[tokio::test] +async fn approve_all_fails_when_managed_settings_enabled() { + let h = approve_all(); + let mut request = data(); + request.managed_settings_enabled = true; + assert!(matches!( + h.handle(SessionId::from("s"), RequestId::new("1"), request) + .await, + PermissionResult::Decision { + decision: crate::types::PermissionDecision::UserNotAvailable(_), + .. + } + )); +} + +#[tokio::test] +async fn deny_all_denies() { + let h = deny_all(); + assert!(matches!( + h.handle(SessionId::from("s"), RequestId::new("1"), data()) + .await, + PermissionResult::Decision { + decision: crate::types::PermissionDecision::Reject(_), + .. + } + )); +} + +#[tokio::test] +async fn approve_if_consults_predicate() { + let h = approve_if(|d| d.extra.get("tool").and_then(|v| v.as_str()) != Some("shell")); + assert!(matches!( + h.handle(SessionId::from("s"), RequestId::new("1"), data()) + .await, + PermissionResult::Decision { + decision: crate::types::PermissionDecision::Reject(_), + .. + } + )); +} + +#[tokio::test] +async fn approve_if_leaves_managed_approval_pending_when_predicate_approves() { + let h = approve_if(|_| true); + let mut request = data(); + request.managed_approval_required = Some(true); + assert!(matches!( + h.handle(SessionId::from("s"), RequestId::new("1"), request) + .await, + PermissionResult::NoResult + )); +} + +#[tokio::test] +async fn approve_if_still_rejects_managed_request_when_predicate_denies() { + let h = approve_if(|_| false); + let mut request = data(); + request.managed_approval_required = Some(true); + assert!(matches!( + h.handle(SessionId::from("s"), RequestId::new("1"), request) + .await, + PermissionResult::Decision { + decision: crate::types::PermissionDecision::Reject(_), + .. + } + )); +} + +#[tokio::test] +async fn resolve_handler_policy_wins() { + struct AlwaysApprove; + #[async_trait] + impl PermissionHandler for AlwaysApprove { + async fn handle( + &self, + _: SessionId, + _: RequestId, + _: PermissionRequestData, + ) -> PermissionResult { + PermissionResult::approve_once() + } + } + let resolved = resolve_handler(Some(Arc::new(AlwaysApprove)), Some(Policy::DenyAll)).unwrap(); + // Policy wins -- the AlwaysApprove handler is discarded. + assert!(matches!( + resolved + .handle(SessionId::from("s"), RequestId::new("1"), data()) + .await, + PermissionResult::Decision { + decision: crate::types::PermissionDecision::Reject(_), + .. + } + )); +} + +#[tokio::test] +async fn resolve_handler_with_only_handler() { + struct H; + #[async_trait] + impl PermissionHandler for H { + async fn handle( + &self, + _: SessionId, + _: RequestId, + _: PermissionRequestData, + ) -> PermissionResult { + PermissionResult::approve_once() + } + } + let resolved = resolve_handler(Some(Arc::new(H)), None).unwrap(); + assert!(matches!( + resolved + .handle(SessionId::from("s"), RequestId::new("1"), data()) + .await, + PermissionResult::Decision { + decision: crate::types::PermissionDecision::ApproveOnce(_), + .. + } + )); +} + +#[test] +fn resolve_handler_with_neither_returns_none() { + assert!(resolve_handler(None, None).is_none()); +} diff --git a/rust/src/resolve.rs b/rust/src/resolve.rs index 5cd04dd502..7809d993d8 100644 --- a/rust/src/resolve.rs +++ b/rust/src/resolve.rs @@ -217,27 +217,4 @@ fn runtime_binary_name() -> &'static str { } #[cfg(test)] -mod tests { - use std::fs; - - use tempfile::tempdir; - - use super::validate_runtime_pair; - - #[test] - fn runtime_pair_requires_adjacent_nonempty_runtime_node() { - let dir = tempdir().expect("temp dir"); - let wrapper = dir.path().join(if cfg!(windows) { - "copilot-runtime.exe" - } else { - "copilot-runtime" - }); - fs::write(&wrapper, b"wrapper").expect("write wrapper"); - - let error = validate_runtime_pair(&wrapper).expect_err("runtime.node is required"); - assert!(error.to_string().contains("runtime.node")); - - fs::write(dir.path().join("runtime.node"), b"runtime").expect("write runtime.node"); - validate_runtime_pair(&wrapper).expect("complete pair is valid"); - } -} +mod tests; diff --git a/rust/src/resolve/tests.rs b/rust/src/resolve/tests.rs new file mode 100644 index 0000000000..526cc08bb6 --- /dev/null +++ b/rust/src/resolve/tests.rs @@ -0,0 +1,28 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +#![cfg(test)] + +use std::fs; + +use tempfile::tempdir; + +use super::validate_runtime_pair; + +#[test] +fn runtime_pair_requires_adjacent_nonempty_runtime_node() { + let dir = tempdir().expect("temp dir"); + let wrapper = dir.path().join(if cfg!(windows) { + "copilot-runtime.exe" + } else { + "copilot-runtime" + }); + fs::write(&wrapper, b"wrapper").expect("write wrapper"); + + let error = validate_runtime_pair(&wrapper).expect_err("runtime.node is required"); + assert!(error.to_string().contains("runtime.node")); + + fs::write(dir.path().join("runtime.node"), b"runtime").expect("write runtime.node"); + validate_runtime_pair(&wrapper).expect("complete pair is valid"); +} diff --git a/rust/src/router.rs b/rust/src/router.rs index 89d7a988fc..02fd5cfde2 100644 --- a/rust/src/router.rs +++ b/rust/src/router.rs @@ -327,35 +327,4 @@ impl SessionRouter { } #[cfg(test)] -mod tests { - use super::*; - - fn session_id() -> SessionId { - SessionId::new("router-ownership") - } - - #[test] - fn each_registration_gets_a_distinct_token() { - let router = SessionRouter::new(); - let first = router.register(&session_id()); - let second = router.register(&session_id()); - assert_ne!(first.token, second.token); - } - - #[test] - fn unregister_owned_removes_only_the_matching_registration() { - let router = SessionRouter::new(); - let stale = router.register(&session_id()); - let live = router.register(&session_id()); - - // The stale owner must not evict the registration that replaced it. - assert!(!router.unregister_owned(&session_id(), stale.token)); - assert_eq!(router.session_ids(), vec![session_id()]); - - assert!(router.unregister_owned(&session_id(), live.token)); - assert!(router.session_ids().is_empty()); - - // Removing twice is a no-op rather than evicting a future tenant. - assert!(!router.unregister_owned(&session_id(), live.token)); - } -} +mod tests; diff --git a/rust/src/router/tests.rs b/rust/src/router/tests.rs new file mode 100644 index 0000000000..4d89fd937f --- /dev/null +++ b/rust/src/router/tests.rs @@ -0,0 +1,36 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +#![cfg(test)] + +use super::*; + +fn session_id() -> SessionId { + SessionId::new("router-ownership") +} + +#[test] +fn each_registration_gets_a_distinct_token() { + let router = SessionRouter::new(); + let first = router.register(&session_id()); + let second = router.register(&session_id()); + assert_ne!(first.token, second.token); +} + +#[test] +fn unregister_owned_removes_only_the_matching_registration() { + let router = SessionRouter::new(); + let stale = router.register(&session_id()); + let live = router.register(&session_id()); + + // The stale owner must not evict the registration that replaced it. + assert!(!router.unregister_owned(&session_id(), stale.token)); + assert_eq!(router.session_ids(), vec![session_id()]); + + assert!(router.unregister_owned(&session_id(), live.token)); + assert!(router.session_ids().is_empty()); + + // Removing twice is a no-op rather than evicting a future tenant. + assert!(!router.unregister_owned(&session_id(), live.token)); +} diff --git a/rust/src/session.rs b/rust/src/session.rs index 4461694570..14864a3137 100644 --- a/rust/src/session.rs +++ b/rust/src/session.rs @@ -1798,6 +1798,15 @@ impl Client { does not implement SessionFsSqliteProvider", )); } + if self.inner.session_fs_binary_declared + && let Some(ref provider) = session_fs_provider + && provider.binary().is_none() + { + return Err(Error::with_message( + ErrorKind::InvalidConfig, + "SessionFs capabilities declare binary support but the provider does not implement SessionFsBinaryProvider", + )); + } let mut params = serde_json::to_value(&wire)?; let trace_ctx = self.resolve_trace_context().await; @@ -2134,6 +2143,15 @@ impl Client { does not implement SessionFsSqliteProvider", )); } + if self.inner.session_fs_binary_declared + && let Some(ref provider) = session_fs_provider + && provider.binary().is_none() + { + return Err(Error::with_message( + ErrorKind::InvalidConfig, + "SessionFs capabilities declare binary support but the provider does not implement SessionFsBinaryProvider", + )); + } let mut params = serde_json::to_value(&wire)?; let trace_ctx = self.resolve_trace_context().await; @@ -3826,311 +3844,7 @@ fn inject_transform_sections_resume( } #[cfg(test)] -mod tests { - use serde_json::json; - - use super::{ - build_mode_post_create_patch, has_managed_settings, is_autopilot_continuation_idle, - permission_request_data, permission_response_params, - }; - use crate::handler::PermissionResult; - use crate::types::{ - PermissionDecisionContext, PermissionDecisionOutcome, PermissionDecisionSource, - PermissionDecisionSurface, RequestId, SessionEvent, SessionId, - }; - - #[test] - fn identifies_only_autopilot_continuation_idles() { - let mut event = SessionEvent { - id: "event-1".to_string(), - timestamp: "2026-01-01T00:00:00Z".to_string(), - parent_id: None, - ephemeral: None, - agent_id: None, - debug_cli_received_at_ms: None, - debug_ws_forwarded_at_ms: None, - event_type: "session.idle".to_string(), - data: json!({ "mode": "autopilot" }), - }; - - assert!(is_autopilot_continuation_idle(&event)); - - event.data = json!({ "mode": "interactive" }); - assert!(!is_autopilot_continuation_idle(&event)); - - event.data = json!({}); - assert!(!is_autopilot_continuation_idle(&event)); - } - - #[test] - fn empty_mode_post_patch_sets_empty_included_builtin_skills() { - let patch = - build_mode_post_create_patch(crate::ClientMode::Empty, None, None, None, None, None) - .expect("empty mode always sends a patch"); - assert_eq!( - patch.included_builtin_skills, - Some(Vec::new()), - "empty mode must fail closed with an empty includedBuiltinSkills list" - ); - assert_eq!(patch.installed_plugins.as_ref().map(|p| p.len()), Some(0)); - // Serializes as an explicit empty array (not omitted). - let value = serde_json::to_value(&patch).expect("serialize patch"); - assert_eq!(value["includedBuiltinSkills"], serde_json::json!([])); - } - - #[test] - fn empty_mode_post_patch_preserves_explicit_builtin_skill_allowlist() { - let patch = build_mode_post_create_patch( - crate::ClientMode::Empty, - Some(false), - Some(false), - Some(true), - Some(true), - Some(vec!["code-review".to_string()]), - ) - .expect("empty mode always sends a patch"); - assert_eq!( - patch.included_builtin_skills, - Some(vec!["code-review".to_string()]) - ); - } - - #[test] - fn copilot_cli_mode_does_not_inject_included_builtin_skills() { - // No fields set -> no patch at all. - assert!( - build_mode_post_create_patch( - crate::ClientMode::CopilotCli, - None, - None, - None, - None, - None - ) - .is_none() - ); - // A field set -> patch sent, but skills field stays absent. - let patch = build_mode_post_create_patch( - crate::ClientMode::CopilotCli, - Some(true), - None, - None, - None, - None, - ) - .expect("a set field triggers a patch"); - assert_eq!(patch.included_builtin_skills, None); - assert!(patch.installed_plugins.is_none()); - let value = serde_json::to_value(&patch).expect("serialize patch"); - assert!(value.get("includedBuiltinSkills").is_none()); - - let patch = build_mode_post_create_patch( - crate::ClientMode::CopilotCli, - None, - None, - None, - None, - Some(vec!["code-review".to_string()]), - ) - .expect("an explicit allowlist triggers a patch"); - assert_eq!( - patch.included_builtin_skills, - Some(vec!["code-review".to_string()]) - ); - } - - #[test] - fn direct_injection_enables_managed_safeguards() { - let settings = crate::types::ManagedSettings::default(); - assert!(has_managed_settings(None, Some(&settings))); - assert!(!has_managed_settings(None, None)); - } - - fn attribution_context() -> PermissionDecisionContext { - PermissionDecisionContext { - outcome: PermissionDecisionOutcome::AutoApproved, - response_capability: None, - source: PermissionDecisionSource::AssistedApproval, - surface: PermissionDecisionSurface::CopilotApp, - } - } - - #[test] - fn response_params_omit_decision_context_without_attribution() { - for (result, expected) in [ - ( - PermissionResult::approve_once(), - json!({ "kind": "approve-once" }), - ), - (PermissionResult::reject(None), json!({ "kind": "reject" })), - ( - PermissionResult::reject(Some("bad".to_string())), - json!({ "kind": "reject", "feedback": "bad" }), - ), - ( - PermissionResult::user_not_available(), - json!({ "kind": "user-not-available" }), - ), - ] { - let params = permission_response_params( - &SessionId::from("session-1"), - &RequestId::from("permission-1"), - &result, - ) - .unwrap(); - assert_eq!( - params, - json!({ - "sessionId": "session-1", - "requestId": "permission-1", - "result": expected, - }) - ); - } - } - - #[test] - fn response_params_forward_decision_context_alongside_result() { - let params = permission_response_params( - &SessionId::from("session-1"), - &RequestId::from("permission-1"), - &PermissionResult::approve_once().with_context(attribution_context()), - ) - .unwrap(); - assert_eq!( - params, - json!({ - "sessionId": "session-1", - "requestId": "permission-1", - "result": { "kind": "approve-once" }, - "decisionContext": { - "outcome": "auto_approved", - "source": "assisted_approval", - "surface": "copilot_app", - }, - }) - ); - // The context is a sibling of `result`, never nested inside it. - assert!(params["result"].get("decisionContext").is_none()); - } - - #[test] - fn response_params_suppressed_for_no_result() { - assert!( - permission_response_params( - &SessionId::from("session-1"), - &RequestId::from("permission-1"), - &PermissionResult::NoResult, - ) - .is_none() - ); - } - - #[test] - fn with_context_is_a_no_op_on_no_result() { - let result = PermissionResult::no_result().with_context(attribution_context()); - assert!(matches!(result, PermissionResult::NoResult)); - } - - #[test] - fn with_context_replaces_rather_than_nests() { - let result = PermissionResult::approve_once() - .with_context(attribution_context()) - .with_context(PermissionDecisionContext { - outcome: PermissionDecisionOutcome::PromptedUser, - response_capability: None, - source: PermissionDecisionSource::HumanResponse, - surface: PermissionDecisionSurface::Sdk, - }); - let params = permission_response_params( - &SessionId::from("session-1"), - &RequestId::from("permission-1"), - &result, - ) - .unwrap(); - assert_eq!( - params["decisionContext"], - json!({ - "outcome": "prompted_user", - "source": "human_response", - "surface": "sdk", - }) - ); - } - - #[test] - fn permission_request_data_reads_nested_managed_approval_metadata() { - let data = permission_request_data( - &json!({ - "requestId": "permission-1", - "permissionRequest": { - "kind": "read", - "managedApprovalRequired": true, - "path": "/workspace/file.txt" - } - }), - false, - ); - - assert_eq!(data.managed_approval_required, Some(true)); - assert_eq!( - data.extra["permissionRequest"]["path"], - "/workspace/file.txt" - ); - } - - #[test] - fn permission_request_data_preserves_managed_flag_when_other_fields_are_malformed() { - let data = permission_request_data( - &json!({ - "requestId": "permission-1", - "permissionRequest": { - "kind": "read", - "managedApprovalRequired": true, - "toolCallId": 42 - } - }), - false, - ); - - assert_eq!(data.managed_approval_required, Some(true)); - assert_eq!(data.extra["requestId"], "permission-1"); - } - - #[test] - fn permission_request_data_fails_closed_for_malformed_managed_flag() { - let data = permission_request_data( - &json!({ - "requestId": "permission-1", - "permissionRequest": { - "kind": "read", - "managedApprovalRequired": "yes", - "path": "/workspace/file.txt" - } - }), - false, - ); - - assert_eq!(data.managed_approval_required, Some(true)); - } - - #[test] - fn permission_request_data_preserves_valid_false_managed_flag() { - let data = permission_request_data( - &json!({ - "requestId": "permission-1", - "permissionRequest": { - "kind": "read", - "managedApprovalRequired": false, - "path": "/workspace/file.txt" - } - }), - false, - ); - - assert_eq!(data.managed_approval_required, Some(false)); - } -} +mod tests; #[cfg(test)] mod startup_tasks_tests; diff --git a/rust/src/session/tests.rs b/rust/src/session/tests.rs new file mode 100644 index 0000000000..e184787d47 --- /dev/null +++ b/rust/src/session/tests.rs @@ -0,0 +1,302 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +#![cfg(test)] + +use serde_json::json; + +use super::{ + build_mode_post_create_patch, has_managed_settings, is_autopilot_continuation_idle, + permission_request_data, permission_response_params, +}; +use crate::handler::PermissionResult; +use crate::types::{ + PermissionDecisionContext, PermissionDecisionOutcome, PermissionDecisionSource, + PermissionDecisionSurface, RequestId, SessionEvent, SessionId, +}; + +#[test] +fn identifies_only_autopilot_continuation_idles() { + let mut event = SessionEvent { + id: "event-1".to_string(), + timestamp: "2026-01-01T00:00:00Z".to_string(), + parent_id: None, + ephemeral: None, + agent_id: None, + debug_cli_received_at_ms: None, + debug_ws_forwarded_at_ms: None, + event_type: "session.idle".to_string(), + data: json!({ "mode": "autopilot" }), + }; + + assert!(is_autopilot_continuation_idle(&event)); + + event.data = json!({ "mode": "interactive" }); + assert!(!is_autopilot_continuation_idle(&event)); + + event.data = json!({}); + assert!(!is_autopilot_continuation_idle(&event)); +} + +#[test] +fn empty_mode_post_patch_sets_empty_included_builtin_skills() { + let patch = + build_mode_post_create_patch(crate::ClientMode::Empty, None, None, None, None, None) + .expect("empty mode always sends a patch"); + assert_eq!( + patch.included_builtin_skills, + Some(Vec::new()), + "empty mode must fail closed with an empty includedBuiltinSkills list" + ); + assert_eq!(patch.installed_plugins.as_ref().map(|p| p.len()), Some(0)); + // Serializes as an explicit empty array (not omitted). + let value = serde_json::to_value(&patch).expect("serialize patch"); + assert_eq!(value["includedBuiltinSkills"], serde_json::json!([])); +} + +#[test] +fn empty_mode_post_patch_preserves_explicit_builtin_skill_allowlist() { + let patch = build_mode_post_create_patch( + crate::ClientMode::Empty, + Some(false), + Some(false), + Some(true), + Some(true), + Some(vec!["code-review".to_string()]), + ) + .expect("empty mode always sends a patch"); + assert_eq!( + patch.included_builtin_skills, + Some(vec!["code-review".to_string()]) + ); +} + +#[test] +fn copilot_cli_mode_does_not_inject_included_builtin_skills() { + // No fields set -> no patch at all. + assert!( + build_mode_post_create_patch(crate::ClientMode::CopilotCli, None, None, None, None, None) + .is_none() + ); + // A field set -> patch sent, but skills field stays absent. + let patch = build_mode_post_create_patch( + crate::ClientMode::CopilotCli, + Some(true), + None, + None, + None, + None, + ) + .expect("a set field triggers a patch"); + assert_eq!(patch.included_builtin_skills, None); + assert!(patch.installed_plugins.is_none()); + let value = serde_json::to_value(&patch).expect("serialize patch"); + assert!(value.get("includedBuiltinSkills").is_none()); + + let patch = build_mode_post_create_patch( + crate::ClientMode::CopilotCli, + None, + None, + None, + None, + Some(vec!["code-review".to_string()]), + ) + .expect("an explicit allowlist triggers a patch"); + assert_eq!( + patch.included_builtin_skills, + Some(vec!["code-review".to_string()]) + ); +} + +#[test] +fn direct_injection_enables_managed_safeguards() { + let settings = crate::types::ManagedSettings::default(); + assert!(has_managed_settings(None, Some(&settings))); + assert!(!has_managed_settings(None, None)); +} + +fn attribution_context() -> PermissionDecisionContext { + PermissionDecisionContext { + outcome: PermissionDecisionOutcome::AutoApproved, + response_capability: None, + source: PermissionDecisionSource::AssistedApproval, + surface: PermissionDecisionSurface::CopilotApp, + } +} + +#[test] +fn response_params_omit_decision_context_without_attribution() { + for (result, expected) in [ + ( + PermissionResult::approve_once(), + json!({ "kind": "approve-once" }), + ), + (PermissionResult::reject(None), json!({ "kind": "reject" })), + ( + PermissionResult::reject(Some("bad".to_string())), + json!({ "kind": "reject", "feedback": "bad" }), + ), + ( + PermissionResult::user_not_available(), + json!({ "kind": "user-not-available" }), + ), + ] { + let params = permission_response_params( + &SessionId::from("session-1"), + &RequestId::from("permission-1"), + &result, + ) + .unwrap(); + assert_eq!( + params, + json!({ + "sessionId": "session-1", + "requestId": "permission-1", + "result": expected, + }) + ); + } +} + +#[test] +fn response_params_forward_decision_context_alongside_result() { + let params = permission_response_params( + &SessionId::from("session-1"), + &RequestId::from("permission-1"), + &PermissionResult::approve_once().with_context(attribution_context()), + ) + .unwrap(); + assert_eq!( + params, + json!({ + "sessionId": "session-1", + "requestId": "permission-1", + "result": { "kind": "approve-once" }, + "decisionContext": { + "outcome": "auto_approved", + "source": "assisted_approval", + "surface": "copilot_app", + }, + }) + ); + // The context is a sibling of `result`, never nested inside it. + assert!(params["result"].get("decisionContext").is_none()); +} + +#[test] +fn response_params_suppressed_for_no_result() { + assert!( + permission_response_params( + &SessionId::from("session-1"), + &RequestId::from("permission-1"), + &PermissionResult::NoResult, + ) + .is_none() + ); +} + +#[test] +fn with_context_is_a_no_op_on_no_result() { + let result = PermissionResult::no_result().with_context(attribution_context()); + assert!(matches!(result, PermissionResult::NoResult)); +} + +#[test] +fn with_context_replaces_rather_than_nests() { + let result = PermissionResult::approve_once() + .with_context(attribution_context()) + .with_context(PermissionDecisionContext { + outcome: PermissionDecisionOutcome::PromptedUser, + response_capability: None, + source: PermissionDecisionSource::HumanResponse, + surface: PermissionDecisionSurface::Sdk, + }); + let params = permission_response_params( + &SessionId::from("session-1"), + &RequestId::from("permission-1"), + &result, + ) + .unwrap(); + assert_eq!( + params["decisionContext"], + json!({ + "outcome": "prompted_user", + "source": "human_response", + "surface": "sdk", + }) + ); +} + +#[test] +fn permission_request_data_reads_nested_managed_approval_metadata() { + let data = permission_request_data( + &json!({ + "requestId": "permission-1", + "permissionRequest": { + "kind": "read", + "managedApprovalRequired": true, + "path": "/workspace/file.txt" + } + }), + false, + ); + + assert_eq!(data.managed_approval_required, Some(true)); + assert_eq!( + data.extra["permissionRequest"]["path"], + "/workspace/file.txt" + ); +} + +#[test] +fn permission_request_data_preserves_managed_flag_when_other_fields_are_malformed() { + let data = permission_request_data( + &json!({ + "requestId": "permission-1", + "permissionRequest": { + "kind": "read", + "managedApprovalRequired": true, + "toolCallId": 42 + } + }), + false, + ); + + assert_eq!(data.managed_approval_required, Some(true)); + assert_eq!(data.extra["requestId"], "permission-1"); +} + +#[test] +fn permission_request_data_fails_closed_for_malformed_managed_flag() { + let data = permission_request_data( + &json!({ + "requestId": "permission-1", + "permissionRequest": { + "kind": "read", + "managedApprovalRequired": "yes", + "path": "/workspace/file.txt" + } + }), + false, + ); + + assert_eq!(data.managed_approval_required, Some(true)); +} + +#[test] +fn permission_request_data_preserves_valid_false_managed_flag() { + let data = permission_request_data( + &json!({ + "requestId": "permission-1", + "permissionRequest": { + "kind": "read", + "managedApprovalRequired": false, + "path": "/workspace/file.txt" + } + }), + false, + ); + + assert_eq!(data.managed_approval_required, Some(false)); +} diff --git a/rust/src/session_fs.rs b/rust/src/session_fs.rs index 1d96daae9e..b65a0d883d 100644 --- a/rust/src/session_fs.rs +++ b/rust/src/session_fs.rs @@ -65,6 +65,8 @@ use crate::{Custom, Repr}; pub struct SessionFsCapabilities { /// Whether the provider supports SQLite query/exists operations. pub sqlite: bool, + /// Whether the provider supports exact binary reads and writes. + pub binary: bool, } impl SessionFsCapabilities { @@ -78,6 +80,12 @@ impl SessionFsCapabilities { self.sqlite = sqlite; self } + + /// Enable binary file reads and writes. + pub fn with_binary(mut self, binary: bool) -> Self { + self.binary = binary; + self + } } /// Configuration for a custom session filesystem provider. @@ -175,6 +183,7 @@ impl fmt::Display for FsErrorKind { #[derive(Debug)] pub struct FsError { repr: Repr, + write_changed: bool, } impl FsError { @@ -188,6 +197,7 @@ impl FsError { kind, error: error.into(), }), + write_changed: false, } } @@ -216,7 +226,24 @@ impl FsError { { Self { repr: Repr::SimpleMessage(kind, message.into()), + write_changed: false, + } + } + + /// Mark a failed `write_file` as having changed its target before failing. + #[must_use] + pub fn with_write_changed(mut self) -> Self { + self.write_changed = true; + self + } + + pub(crate) fn into_write_wire(self) -> SessionFsError { + let changed = self.write_changed; + let mut wire = self.into_wire(); + if changed { + wire.write_changed = Some(true); } + wire } pub(crate) fn into_wire(self) -> SessionFsError { @@ -224,10 +251,12 @@ impl FsError { FsErrorKind::NotFound(message) => SessionFsError { code: SessionFsErrorCode::ENOENT, message: Some(message.clone()), + write_changed: None, }, FsErrorKind::Other => SessionFsError { code: SessionFsErrorCode::UNKNOWN, message: Some(self.to_string()), + write_changed: None, }, } } @@ -256,6 +285,7 @@ impl From for FsError { fn from(kind: FsErrorKind) -> Self { Self { repr: Repr::Simple(kind), + write_changed: false, } } } @@ -402,7 +432,13 @@ pub trait SessionFsProvider: Send + Sync + 'static { )) } - /// Write content to a file, creating parent directories if needed. + /// Return an optional binary implementation when this provider supports it. + fn binary(&self) -> Option<&dyn SessionFsBinaryProvider> { + None + } + + /// Write content to a file, creating parent directories if needed. Mark a failed + /// write with [`FsError::with_write_changed`] only if it changed the target. async fn write_file( &self, path: &str, @@ -505,6 +541,21 @@ pub trait SessionFsProvider: Send + Sync + 'static { } } +/// Optional exact-byte reads and writes for providers declaring `binary`. +#[async_trait] +pub trait SessionFsBinaryProvider: Send + Sync { + /// Read exact bytes of the file at `path`. + async fn read_file_bytes(&self, path: &str) -> Result, FsError>; + + /// Write exact bytes to the file at `path`, using `mode` when supplied. + async fn write_file_bytes( + &self, + path: &str, + content: &[u8], + mode: Option, + ) -> Result<(), FsError>; +} + /// Optional trait for providers that support SQLite operations. /// /// Providers are already session-scoped (created per session by the factory), @@ -621,52 +672,7 @@ pub struct SessionFsSqliteQueryResult { } #[cfg(test)] -mod tests { - use super::*; - - #[test] - fn fs_error_maps_io_not_found_to_enoent() { - let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "missing.txt"); - let fs_err: FsError = io_err.into(); - assert!( - matches!(fs_err.kind(), FsErrorKind::NotFound(message) if message == "missing.txt") - ); - let wire = fs_err.into_wire(); - assert_eq!(wire.code, SessionFsErrorCode::ENOENT); - } - - #[test] - fn fs_error_maps_other_io_to_unknown() { - let io_err = std::io::Error::other("disk full"); - let fs_err: FsError = io_err.into(); - assert!(matches!(fs_err.kind(), FsErrorKind::Other)); - let wire = fs_err.into_wire(); - assert_eq!(wire.code, SessionFsErrorCode::UNKNOWN); - assert!(wire.message.unwrap().contains("disk full")); - } - - #[test] - fn conventions_maps_to_wire() { - assert_eq!( - SessionFsConventions::Posix.into_wire(), - SessionFsSetProviderConventions::Posix - ); - assert_eq!( - SessionFsConventions::Windows.into_wire(), - SessionFsSetProviderConventions::Windows - ); - } - - struct DefaultProvider; - #[async_trait] - impl SessionFsProvider for DefaultProvider {} - - #[tokio::test] - async fn default_impls_return_unsupported() { - let p = DefaultProvider; - let err = p.read_file("/x").await.unwrap_err(); - assert!( - matches!(err.kind(), FsErrorKind::Other) && err.to_string().contains("not supported") - ); - } -} +mod tests; + +#[cfg(test)] +mod write_failure_tests; diff --git a/rust/src/session_fs/tests.rs b/rust/src/session_fs/tests.rs new file mode 100644 index 0000000000..7736636d02 --- /dev/null +++ b/rust/src/session_fs/tests.rs @@ -0,0 +1,49 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +#![cfg(test)] + +use super::*; + +#[test] +fn fs_error_maps_io_not_found_to_enoent() { + let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "missing.txt"); + let fs_err: FsError = io_err.into(); + assert!(matches!(fs_err.kind(), FsErrorKind::NotFound(message) if message == "missing.txt")); + let wire = fs_err.into_wire(); + assert_eq!(wire.code, SessionFsErrorCode::ENOENT); +} + +#[test] +fn fs_error_maps_other_io_to_unknown() { + let io_err = std::io::Error::other("disk full"); + let fs_err: FsError = io_err.into(); + assert!(matches!(fs_err.kind(), FsErrorKind::Other)); + let wire = fs_err.into_wire(); + assert_eq!(wire.code, SessionFsErrorCode::UNKNOWN); + assert!(wire.message.unwrap().contains("disk full")); +} + +#[test] +fn conventions_maps_to_wire() { + assert_eq!( + SessionFsConventions::Posix.into_wire(), + SessionFsSetProviderConventions::Posix + ); + assert_eq!( + SessionFsConventions::Windows.into_wire(), + SessionFsSetProviderConventions::Windows + ); +} + +struct DefaultProvider; +#[async_trait] +impl SessionFsProvider for DefaultProvider {} + +#[tokio::test] +async fn default_impls_return_unsupported() { + let p = DefaultProvider; + let err = p.read_file("/x").await.unwrap_err(); + assert!(matches!(err.kind(), FsErrorKind::Other) && err.to_string().contains("not supported")); +} diff --git a/rust/src/session_fs/write_failure_tests.rs b/rust/src/session_fs/write_failure_tests.rs new file mode 100644 index 0000000000..905781a0db --- /dev/null +++ b/rust/src/session_fs/write_failure_tests.rs @@ -0,0 +1,26 @@ +#![cfg(test)] + +use super::{FsError, FsErrorKind, SessionFsErrorCode}; + +#[test] +fn failed_write_reports_only_known_changes() { + let ordinary = FsError::with_message(FsErrorKind::Other, "rejected").into_write_wire(); + assert_eq!(ordinary.code, SessionFsErrorCode::UNKNOWN); + assert_eq!(ordinary.write_changed, None); + + let changed = FsError::with_message(FsErrorKind::Other, "disk full") + .with_write_changed() + .into_write_wire(); + assert_eq!(changed.message.as_deref(), Some("disk full")); + assert_eq!(changed.write_changed, Some(true)); + let json = serde_json::to_value(changed).unwrap(); + assert_eq!(json["writeChanged"], true); +} + +#[test] +fn write_marker_does_not_leak_into_other_methods() { + let read_error = FsError::with_message(FsErrorKind::Other, "read failed") + .with_write_changed() + .into_wire(); + assert_eq!(read_error.write_changed, None); +} diff --git a/rust/src/session_fs_dispatch.rs b/rust/src/session_fs_dispatch.rs index c84981ac0c..06cbb016b9 100644 --- a/rust/src/session_fs_dispatch.rs +++ b/rust/src/session_fs_dispatch.rs @@ -7,25 +7,29 @@ use std::sync::Arc; +use base64::Engine as _; use serde::Serialize; use serde_json::Value; use tracing::warn; use crate::generated::api_types::{ SessionFsAppendFileRequest, SessionFsError, SessionFsErrorCode, SessionFsExistsRequest, - SessionFsExistsResult, SessionFsMkdirRequest, SessionFsReadFileRequest, - SessionFsReadFileResult, SessionFsReaddirRequest, SessionFsReaddirResult, - SessionFsReaddirWithTypesRequest, SessionFsReaddirWithTypesResult, SessionFsRenameRequest, - SessionFsRmRequest, SessionFsSqliteExistsParams, SessionFsSqliteExistsResult, - SessionFsSqliteQueryRequest, SessionFsSqliteQueryResult as GeneratedSqliteQueryResult, + SessionFsExistsResult, SessionFsMkdirRequest, SessionFsReadFileBytesRequest, + SessionFsReadFileBytesResult, SessionFsReadFileRequest, SessionFsReadFileResult, + SessionFsReaddirRequest, SessionFsReaddirResult, SessionFsReaddirWithTypesRequest, + SessionFsReaddirWithTypesResult, SessionFsRenameRequest, SessionFsRmRequest, + SessionFsSqliteExistsParams, SessionFsSqliteExistsResult, SessionFsSqliteQueryRequest, + SessionFsSqliteQueryResult as GeneratedSqliteQueryResult, SessionFsSqliteTransactionError as GeneratedSqliteTransactionError, SessionFsSqliteTransactionErrorClass, SessionFsSqliteTransactionRequest, SessionFsSqliteTransactionResult as GeneratedSqliteTransactionResult, SessionFsStatRequest, - SessionFsStatResult, SessionFsWriteFileRequest, + SessionFsStatResult, SessionFsWriteFileBytesRequest, SessionFsWriteFileRequest, }; use crate::session_fs::SessionFsProvider; use crate::{Client, JsonRpcRequest, JsonRpcResponse, error_codes}; +const MAX_BINARY_BYTES: usize = (64 * 1024 * 1024 - 1024) / 4 * 3; + /// Helper: serialize a typed result, send the response. async fn respond(client: &Client, request_id: u64, result: T) { let value = match serde_json::to_value(&result) { @@ -94,6 +98,53 @@ pub(crate) async fn read_file( respond(client, id, result).await; } +pub(crate) async fn read_file_bytes( + client: &Client, + provider: &Arc, + request: JsonRpcRequest, +) { + let params: SessionFsReadFileBytesRequest = match parse_params(&request) { + Some(params) => params, + None => { + send_error(client, request.id, "invalid sessionFs.readFileBytes params").await; + return; + } + }; + let result = match provider.binary() { + Some(binary) => binary.read_file_bytes(¶ms.path).await, + None => Err(crate::session_fs::FsError::with_message( + crate::session_fs::FsErrorKind::Other, + "binary reads are not supported", + )), + }; + respond(client, request.id, binary_read_response(result)).await; +} + +fn binary_read_response( + result: Result, crate::session_fs::FsError>, +) -> SessionFsReadFileBytesResult { + match result { + Ok(bytes) if bytes.len() > MAX_BINARY_BYTES => SessionFsReadFileBytesResult { + content: String::new(), + error: Some( + crate::session_fs::FsError::with_message( + crate::session_fs::FsErrorKind::Other, + "sessionFs.readFileBytes content exceeds the binary read limit", + ) + .into_wire(), + ), + }, + Ok(bytes) => SessionFsReadFileBytesResult { + content: base64::engine::general_purpose::STANDARD.encode(bytes), + error: None, + }, + Err(error) => SessionFsReadFileBytesResult { + content: String::new(), + error: Some(error.into_wire()), + }, + } +} + pub(crate) async fn write_file( client: &Client, provider: &Arc, @@ -112,7 +163,54 @@ pub(crate) async fn write_file( .await { Ok(()) => respond(client, id, Value::Null).await, - Err(e) => respond(client, id, e.into_wire()).await, + Err(e) => respond(client, id, e.into_write_wire()).await, + } +} + +pub(crate) async fn write_file_bytes( + client: &Client, + provider: &Arc, + request: JsonRpcRequest, +) { + let params: SessionFsWriteFileBytesRequest = match parse_params(&request) { + Some(params) => params, + None => { + send_error( + client, + request.id, + "invalid sessionFs.writeFileBytes params", + ) + .await; + return; + } + }; + let result = if params.content.len() > MAX_BINARY_BYTES.div_ceil(3) * 4 { + Err(crate::session_fs::FsError::with_message( + crate::session_fs::FsErrorKind::Other, + "sessionFs.writeFileBytes content exceeds the binary write limit", + )) + } else { + match base64::engine::general_purpose::STANDARD.decode(¶ms.content) { + Ok(content) => match provider.binary() { + Some(binary) => { + binary + .write_file_bytes(¶ms.path, &content, params.mode) + .await + } + None => Err(crate::session_fs::FsError::with_message( + crate::session_fs::FsErrorKind::Other, + "binary writes are not supported", + )), + }, + Err(error) => Err(crate::session_fs::FsError::with_message( + crate::session_fs::FsErrorKind::Other, + format!("invalid sessionFs.writeFileBytes base64 content: {error}"), + )), + } + }; + match result { + Ok(()) => respond(client, request.id, Value::Null).await, + Err(error) => respond(client, request.id, error.into_wire()).await, } } @@ -334,6 +432,7 @@ pub(crate) async fn sqlite_query( message: Some( "SQLite is not supported by this SessionFs provider".to_string(), ), + write_changed: None, }), last_insert_rowid: None, rows: Vec::new(), @@ -487,6 +586,8 @@ pub(crate) async fn dispatch( }; match method { "sessionFs.readFile" => read_file(client, &provider, request).await, + "sessionFs.readFileBytes" => read_file_bytes(client, &provider, request).await, + "sessionFs.writeFileBytes" => write_file_bytes(client, &provider, request).await, "sessionFs.writeFile" => write_file(client, &provider, request).await, "sessionFs.appendFile" => append_file(client, &provider, request).await, "sessionFs.exists" => exists(client, &provider, request).await, @@ -506,3 +607,6 @@ pub(crate) async fn dispatch( } true } + +#[cfg(test)] +mod tests; diff --git a/rust/src/session_fs_dispatch/tests.rs b/rust/src/session_fs_dispatch/tests.rs new file mode 100644 index 0000000000..c0f70d78b9 --- /dev/null +++ b/rust/src/session_fs_dispatch/tests.rs @@ -0,0 +1,26 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. + +#![cfg(test)] + +use super::*; + +#[test] +fn oversized_binary_provider_result_is_a_filesystem_error() { + let result = binary_read_response(Ok(vec![0; MAX_BINARY_BYTES + 1])); + assert!(result.content.is_empty()); + let error = result.error.expect("oversized result must carry an error"); + assert_eq!(error.code, SessionFsErrorCode::UNKNOWN); + assert!( + error + .message + .as_deref() + .is_some_and(|message| message.contains("binary read limit")) + ); +} + +#[test] +fn small_binary_provider_result_preserves_exact_bytes() { + let result = binary_read_response(Ok(vec![0, 255, 254, 1])); + assert_eq!(result.content, "AP/+AQ=="); + assert!(result.error.is_none()); +} diff --git a/rust/src/startup_timings.rs b/rust/src/startup_timings.rs index 4202b1e465..bf0166772d 100644 --- a/rust/src/startup_timings.rs +++ b/rust/src/startup_timings.rs @@ -81,27 +81,4 @@ impl StartupTimings { } #[cfg(test)] -mod tests { - use super::*; - - #[test] - fn millis_truncates_to_whole_milliseconds() { - assert_eq!(StartupTimings::millis(Duration::from_micros(1_999)), 1); - assert_eq!(StartupTimings::millis(Duration::from_millis(250)), 250); - assert_eq!(StartupTimings::millis(Duration::ZERO), 0); - } - - #[test] - fn default_leaves_every_phase_unset() { - let timings = StartupTimings::default(); - assert_eq!(timings, StartupTimings::default()); - assert!(timings.program_resolve_ms.is_none()); - assert!(timings.process_spawn_ms.is_none()); - assert!(timings.port_wait_ms.is_none()); - assert_eq!(timings.transport_setup_ms, 0); - assert_eq!(timings.handshake_ms, 0); - assert!(timings.session_fs_ms.is_none()); - assert!(timings.llm_handler_ms.is_none()); - assert_eq!(timings.total_ms, 0); - } -} +mod tests; diff --git a/rust/src/startup_timings/tests.rs b/rust/src/startup_timings/tests.rs new file mode 100644 index 0000000000..f59f60ac04 --- /dev/null +++ b/rust/src/startup_timings/tests.rs @@ -0,0 +1,28 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +#![cfg(test)] + +use super::*; + +#[test] +fn millis_truncates_to_whole_milliseconds() { + assert_eq!(StartupTimings::millis(Duration::from_micros(1_999)), 1); + assert_eq!(StartupTimings::millis(Duration::from_millis(250)), 250); + assert_eq!(StartupTimings::millis(Duration::ZERO), 0); +} + +#[test] +fn default_leaves_every_phase_unset() { + let timings = StartupTimings::default(); + assert_eq!(timings, StartupTimings::default()); + assert!(timings.program_resolve_ms.is_none()); + assert!(timings.process_spawn_ms.is_none()); + assert!(timings.port_wait_ms.is_none()); + assert_eq!(timings.transport_setup_ms, 0); + assert_eq!(timings.handshake_ms, 0); + assert!(timings.session_fs_ms.is_none()); + assert!(timings.llm_handler_ms.is_none()); + assert_eq!(timings.total_ms, 0); +} diff --git a/rust/src/tests.rs b/rust/src/tests.rs new file mode 100644 index 0000000000..8656d6f4d3 --- /dev/null +++ b/rust/src/tests.rs @@ -0,0 +1,813 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +#![cfg(test)] + +use super::*; + +#[test] +fn is_transport_failure_matches_request_cancelled() { + let err = Error::from(ErrorKind::Protocol(ProtocolErrorKind::RequestCancelled)); + assert!(err.is_transport_failure()); +} + +#[test] +fn is_transport_failure_matches_io_error() { + let err = Error::from(std::io::Error::new(std::io::ErrorKind::BrokenPipe, "gone")); + assert!(err.is_transport_failure()); +} + +#[test] +fn is_transport_failure_rejects_rpc_error() { + let err = Error::with_message(ErrorKind::Rpc { code: -1 }, "bad"); + assert!(!err.is_transport_failure()); +} + +#[test] +fn is_transport_failure_rejects_session_error() { + let err = Error::from(ErrorKind::Session(SessionErrorKind::NotFound("s1".into()))); + assert!(!err.is_transport_failure()); +} + +#[test] +fn client_options_builder_composes() { + let opts = ClientOptions::new() + .with_program(CliProgram::Path(PathBuf::from("/usr/local/bin/copilot"))) + .with_prefix_args(["node"]) + .with_cwd(PathBuf::from("/tmp")) + .with_env([("KEY", "value")]) + .with_env_remove(["UNWANTED"]) + .with_extra_args(["--quiet"]) + .with_github_token("ghp_test") + .with_use_logged_in_user(false) + .with_log_level(LogLevel::Debug) + .with_session_idle_timeout_seconds(120) + .with_enable_remote_sessions(true); + assert!(matches!(opts.program, CliProgram::Path(_))); + assert_eq!(opts.prefix_args, vec![std::ffi::OsString::from("node")]); + assert_eq!(opts.working_directory, PathBuf::from("/tmp")); + assert_eq!( + opts.env, + vec![( + std::ffi::OsString::from("KEY"), + std::ffi::OsString::from("value") + )] + ); + assert_eq!(opts.env_remove, vec![std::ffi::OsString::from("UNWANTED")]); + assert_eq!(opts.extra_args, vec!["--quiet".to_string()]); + assert_eq!(opts.github_token.as_deref(), Some("ghp_test")); + assert_eq!(opts.use_logged_in_user, Some(false)); + assert!(matches!(opts.log_level, Some(LogLevel::Debug))); + assert_eq!(opts.session_idle_timeout_seconds, Some(120)); + assert!(opts.enable_remote_sessions); +} + +#[test] +fn default_transport_values_resolve_without_process_state() { + assert!(matches!( + resolve_default_transport_value(None).unwrap(), + Transport::Stdio + )); + assert!(matches!( + resolve_default_transport_value(Some("stdio")).unwrap(), + Transport::Stdio + )); + assert!(matches!( + resolve_default_transport_value(Some("INPROCESS")).unwrap(), + Transport::InProcess + )); + assert!(resolve_default_transport_value(Some("tcp")).is_err()); +} + +#[test] +fn inprocess_rejects_process_scoped_options() { + let invalid = [ + ClientOptions::new().with_cwd("."), + ClientOptions::new().with_env([("KEY", "value")]), + ClientOptions::new().with_env_remove(["KEY"]), + ClientOptions::new().with_telemetry(TelemetryConfig::default()), + ClientOptions::new().with_prefix_args(["index.js"]), + ClientOptions::new().with_program(CliProgram::Path("copilot".into())), + ClientOptions::new().with_extra_args(["--verbose"]), + ]; + + for options in invalid { + assert!(validate_inprocess_options(&options).is_err()); + } +} + +#[test] +fn inprocess_allows_typed_runtime_options() { + let options = ClientOptions::new() + .with_base_directory("state") + .with_log_level(LogLevel::Debug) + .with_session_idle_timeout_seconds(10) + .with_github_token("token") + .with_use_logged_in_user(false) + .with_enable_remote_sessions(true); + + assert!(validate_inprocess_options(&options).is_ok()); +} + +#[cfg(not(feature = "in-process"))] +#[tokio::test] +async fn inprocess_requires_cargo_feature() { + let error = Client::start(ClientOptions::new().with_transport(Transport::InProcess)) + .await + .unwrap_err(); + + assert!(error.to_string().contains("in-process")); +} + +#[test] +fn is_transport_failure_rejects_other_protocol_errors() { + let err = Error::from(ErrorKind::Protocol(ProtocolErrorKind::CliStartupTimeout)); + assert!(!err.is_transport_failure()); +} + +#[test] +fn build_command_lets_env_remove_strip_injected_token() { + let opts = ClientOptions { + github_token: Some("secret".to_string()), + env_remove: vec![std::ffi::OsString::from("COPILOT_SDK_AUTH_TOKEN")], + ..Default::default() + }; + let cmd = Client::build_command(Path::new("/bin/echo"), &opts, Path::new("/tmp")); + // get_envs() iter yields the latest action per key — None means removed. + let action = cmd + .as_std() + .get_envs() + .find(|(k, _)| *k == std::ffi::OsStr::new("COPILOT_SDK_AUTH_TOKEN")) + .map(|(_, v)| v); + assert_eq!( + action, + Some(None), + "env_remove should win over github_token" + ); +} + +#[test] +fn build_command_lets_env_override_injected_token() { + let opts = ClientOptions { + github_token: Some("from-options".to_string()), + env: vec![( + std::ffi::OsString::from("COPILOT_SDK_AUTH_TOKEN"), + std::ffi::OsString::from("from-env"), + )], + ..Default::default() + }; + let cmd = Client::build_command(Path::new("/bin/echo"), &opts, Path::new("/tmp")); + let value = cmd + .as_std() + .get_envs() + .find(|(k, _)| *k == std::ffi::OsStr::new("COPILOT_SDK_AUTH_TOKEN")) + .and_then(|(_, v)| v); + assert_eq!(value, Some(std::ffi::OsStr::new("from-env"))); +} + +#[test] +fn build_command_injects_github_token_by_default() { + let opts = ClientOptions { + github_token: Some("just-the-token".to_string()), + ..Default::default() + }; + let cmd = Client::build_command(Path::new("/bin/echo"), &opts, Path::new("/tmp")); + let value = cmd + .as_std() + .get_envs() + .find(|(k, _)| *k == std::ffi::OsStr::new("COPILOT_SDK_AUTH_TOKEN")) + .and_then(|(_, v)| v); + assert_eq!(value, Some(std::ffi::OsStr::new("just-the-token"))); +} + +fn env_value<'a>(cmd: &'a tokio::process::Command, key: &str) -> Option<&'a std::ffi::OsStr> { + cmd.as_std() + .get_envs() + .find(|(k, _)| *k == std::ffi::OsStr::new(key)) + .and_then(|(_, v)| v) +} + +#[test] +fn telemetry_config_builder_composes() { + let cfg = TelemetryConfig::new() + .with_otlp_endpoint("http://collector:4318") + .with_otlp_protocol(OtlpHttpProtocol::HttpProtobuf) + .with_file_path(PathBuf::from("/var/log/copilot.jsonl")) + .with_exporter_type(OtelExporterType::OtlpHttp) + .with_source_name("my-app") + .with_capture_content(true); + + assert_eq!(cfg.otlp_endpoint.as_deref(), Some("http://collector:4318")); + assert_eq!(cfg.otlp_protocol, Some(OtlpHttpProtocol::HttpProtobuf)); + assert_eq!( + cfg.file_path.as_deref(), + Some(Path::new("/var/log/copilot.jsonl")), + ); + assert_eq!(cfg.exporter_type, Some(OtelExporterType::OtlpHttp)); + assert_eq!(cfg.source_name.as_deref(), Some("my-app")); + assert_eq!(cfg.capture_content, Some(true)); + assert!(!cfg.is_empty()); + assert!(TelemetryConfig::new().is_empty()); +} + +#[test] +fn otlp_http_protocol_serde_matches_env_value() { + for (protocol, wire) in [ + (OtlpHttpProtocol::HttpJson, "http/json"), + (OtlpHttpProtocol::HttpProtobuf, "http/protobuf"), + ] { + assert_eq!(protocol.as_str(), wire); + + let serialized = serde_json::to_string(&protocol).unwrap(); + assert_eq!(serialized, format!("\"{wire}\"")); + + let deserialized: OtlpHttpProtocol = serde_json::from_str(&serialized).unwrap(); + assert_eq!(deserialized, protocol); + } +} + +#[test] +fn build_command_sets_otel_env_when_telemetry_enabled() { + let opts = ClientOptions { + telemetry: Some(TelemetryConfig { + otlp_endpoint: Some("http://collector:4318".to_string()), + otlp_protocol: Some(OtlpHttpProtocol::HttpProtobuf), + file_path: Some(PathBuf::from("/var/log/copilot.jsonl")), + exporter_type: Some(OtelExporterType::OtlpHttp), + source_name: Some("my-app".to_string()), + capture_content: Some(true), + }), + ..Default::default() + }; + let cmd = Client::build_command(Path::new("/bin/echo"), &opts, Path::new("/tmp")); + assert_eq!( + env_value(&cmd, "COPILOT_OTEL_ENABLED"), + Some(std::ffi::OsStr::new("true")), + ); + assert_eq!( + env_value(&cmd, "OTEL_EXPORTER_OTLP_ENDPOINT"), + Some(std::ffi::OsStr::new("http://collector:4318")), + ); + assert_eq!( + env_value(&cmd, "OTEL_EXPORTER_OTLP_PROTOCOL"), + Some(std::ffi::OsStr::new("http/protobuf")), + ); + assert_eq!( + env_value(&cmd, "COPILOT_OTEL_FILE_EXPORTER_PATH"), + Some(std::ffi::OsStr::new("/var/log/copilot.jsonl")), + ); + assert_eq!( + env_value(&cmd, "COPILOT_OTEL_EXPORTER_TYPE"), + Some(std::ffi::OsStr::new("otlp-http")), + ); + assert_eq!( + env_value(&cmd, "COPILOT_OTEL_SOURCE_NAME"), + Some(std::ffi::OsStr::new("my-app")), + ); + assert_eq!( + env_value(&cmd, "OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT"), + Some(std::ffi::OsStr::new("true")), + ); +} + +#[test] +fn build_command_omits_otel_env_when_telemetry_none() { + let opts = ClientOptions::default(); + let cmd = Client::build_command(Path::new("/bin/echo"), &opts, Path::new("/tmp")); + for key in [ + "COPILOT_OTEL_ENABLED", + "OTEL_EXPORTER_OTLP_ENDPOINT", + "OTEL_EXPORTER_OTLP_PROTOCOL", + "COPILOT_OTEL_FILE_EXPORTER_PATH", + "COPILOT_OTEL_EXPORTER_TYPE", + "COPILOT_OTEL_SOURCE_NAME", + "OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT", + ] { + assert!( + env_value(&cmd, key).is_none(), + "expected {key} to be unset when telemetry is None", + ); + } +} + +#[test] +fn build_command_omits_unset_telemetry_fields() { + let opts = ClientOptions { + telemetry: Some(TelemetryConfig { + otlp_endpoint: Some("http://collector:4318".to_string()), + ..Default::default() + }), + ..Default::default() + }; + let cmd = Client::build_command(Path::new("/bin/echo"), &opts, Path::new("/tmp")); + // The one set field plus the implicit enabled flag should propagate. + assert_eq!( + env_value(&cmd, "COPILOT_OTEL_ENABLED"), + Some(std::ffi::OsStr::new("true")), + ); + assert_eq!( + env_value(&cmd, "OTEL_EXPORTER_OTLP_ENDPOINT"), + Some(std::ffi::OsStr::new("http://collector:4318")), + ); + // None of the other fields should leak as env vars. + for key in [ + "OTEL_EXPORTER_OTLP_PROTOCOL", + "COPILOT_OTEL_FILE_EXPORTER_PATH", + "COPILOT_OTEL_EXPORTER_TYPE", + "COPILOT_OTEL_SOURCE_NAME", + "OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT", + ] { + assert!(env_value(&cmd, key).is_none(), "{key} should be unset"); + } +} + +#[test] +fn build_command_lets_user_env_override_telemetry() { + let opts = ClientOptions { + telemetry: Some(TelemetryConfig { + otlp_endpoint: Some("http://from-config:4318".to_string()), + ..Default::default() + }), + env: vec![( + std::ffi::OsString::from("OTEL_EXPORTER_OTLP_ENDPOINT"), + std::ffi::OsString::from("http://from-user-env:4318"), + )], + ..Default::default() + }; + let cmd = Client::build_command(Path::new("/bin/echo"), &opts, Path::new("/tmp")); + assert_eq!( + env_value(&cmd, "OTEL_EXPORTER_OTLP_ENDPOINT"), + Some(std::ffi::OsStr::new("http://from-user-env:4318")), + "user-supplied options.env should override telemetry config", + ); +} + +#[test] +fn build_command_sets_copilot_home_env_when_configured() { + let opts = ClientOptions::new().with_base_directory(PathBuf::from("/custom/copilot")); + let cmd = Client::build_command(Path::new("/bin/echo"), &opts, Path::new("/tmp")); + assert_eq!( + env_value(&cmd, "COPILOT_HOME"), + Some(std::ffi::OsStr::new("/custom/copilot")), + ); + + let opts = ClientOptions::default(); + let cmd = Client::build_command(Path::new("/bin/echo"), &opts, Path::new("/tmp")); + assert!(env_value(&cmd, "COPILOT_HOME").is_none()); +} + +#[test] +fn build_command_sets_connection_token_env_when_configured() { + let opts = ClientOptions::new().with_transport(Transport::Tcp { + port: 0, + connection_token: Some("secret-token".to_string()), + }); + let cmd = Client::build_command(Path::new("/bin/echo"), &opts, Path::new("/tmp")); + assert_eq!( + env_value(&cmd, "COPILOT_CONNECTION_TOKEN"), + Some(std::ffi::OsStr::new("secret-token")), + ); + + let opts = ClientOptions::default(); + let cmd = Client::build_command(Path::new("/bin/echo"), &opts, Path::new("/tmp")); + assert!(env_value(&cmd, "COPILOT_CONNECTION_TOKEN").is_none()); +} + +#[tokio::test] +async fn start_rejects_empty_connection_token() { + let opts = ClientOptions::new() + .with_transport(Transport::Tcp { + port: 0, + connection_token: Some(String::new()), + }) + .with_program(CliProgram::Path(PathBuf::from("/bin/echo"))); + let err = Client::start(opts).await.unwrap_err(); + assert!( + matches!(err.kind(), ErrorKind::InvalidConfig), + "got {err:?}" + ); +} + +#[tokio::test] +async fn start_rejects_empty_external_connection_token() { + let opts = ClientOptions::new() + .with_transport(Transport::External { + host: "127.0.0.1".to_string(), + port: 1, + connection_token: Some(String::new()), + }) + .with_program(CliProgram::Path(PathBuf::from("/bin/echo"))); + let err = Client::start(opts).await.unwrap_err(); + assert!( + matches!(err.kind(), ErrorKind::InvalidConfig), + "got {err:?}" + ); +} + +#[test] +fn telemetry_config_capture_content_serializes_as_lowercase_bool() { + let opts_true = ClientOptions { + telemetry: Some(TelemetryConfig { + capture_content: Some(true), + ..Default::default() + }), + ..Default::default() + }; + let opts_false = ClientOptions { + telemetry: Some(TelemetryConfig { + capture_content: Some(false), + ..Default::default() + }), + ..Default::default() + }; + let cmd_true = Client::build_command(Path::new("/bin/echo"), &opts_true, Path::new("/tmp")); + let cmd_false = Client::build_command(Path::new("/bin/echo"), &opts_false, Path::new("/tmp")); + assert_eq!( + env_value( + &cmd_true, + "OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT" + ), + Some(std::ffi::OsStr::new("true")), + ); + assert_eq!( + env_value( + &cmd_false, + "OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT" + ), + Some(std::ffi::OsStr::new("false")), + ); +} + +#[test] +fn session_idle_timeout_args_are_omitted_by_default() { + let opts = ClientOptions::default(); + assert!(Client::session_idle_timeout_args(&opts).is_empty()); +} + +#[test] +fn session_idle_timeout_args_omitted_for_zero() { + let opts = ClientOptions { + session_idle_timeout_seconds: Some(0), + ..Default::default() + }; + assert!(Client::session_idle_timeout_args(&opts).is_empty()); +} + +#[test] +fn session_idle_timeout_args_emit_flag_for_positive_value() { + let opts = ClientOptions { + session_idle_timeout_seconds: Some(300), + ..Default::default() + }; + assert_eq!( + Client::session_idle_timeout_args(&opts), + vec!["--session-idle-timeout".to_string(), "300".to_string()] + ); +} + +#[test] +fn remote_args_omitted_by_default() { + let opts = ClientOptions::default(); + assert!(Client::remote_args(&opts).is_empty()); +} + +#[test] +fn remote_args_emit_flag_when_enabled() { + let opts = ClientOptions { + enable_remote_sessions: true, + ..Default::default() + }; + assert_eq!(Client::remote_args(&opts), vec!["--remote".to_string()]); +} + +#[test] +fn log_level_args_omitted_when_unset() { + let opts = ClientOptions::default(); + assert!(opts.log_level.is_none()); + assert!( + Client::log_level_args(&opts).is_empty(), + "with no caller-supplied log_level the SDK must not pass --log-level" + ); +} + +#[test] +fn log_level_args_emit_flag_when_set() { + let opts = ClientOptions::default().with_log_level(LogLevel::Debug); + assert_eq!(Client::log_level_args(&opts), vec!["--log-level", "debug"]); +} + +#[test] +fn cli_mode_opts_into_process_logging_without_changing_empty_mode_environment() { + for (mode, expected) in [ + (ClientMode::Empty, None), + (ClientMode::CopilotCli, Some("1")), + ] { + let mut options = ClientOptions::default().with_mode(mode); + options.env.push(( + std::ffi::OsString::from("COPILOT_RUNTIME_PROCESS_FILE_LOGGING"), + std::ffi::OsString::from("opposite"), + )); + let command = Client::build_command(Path::new("copilot-runtime"), &options, Path::new(".")); + let actual = command + .as_std() + .get_envs() + .find(|(key, _)| *key == std::ffi::OsStr::new("COPILOT_RUNTIME_PROCESS_FILE_LOGGING")) + .and_then(|(_, value)| value); + assert_eq!( + actual, + Some(std::ffi::OsStr::new(expected.unwrap_or("opposite"))), + "mode: {mode:?}" + ); + } +} + +#[test] +fn log_level_str_round_trips() { + for level in [ + LogLevel::None, + LogLevel::Error, + LogLevel::Warning, + LogLevel::Info, + LogLevel::Debug, + LogLevel::All, + ] { + let s = level.as_str(); + let json = serde_json::to_string(&level).unwrap(); + assert_eq!(json, format!("\"{s}\"")); + let parsed: LogLevel = serde_json::from_str(&json).unwrap(); + assert_eq!(parsed, level); + } +} + +#[test] +fn client_options_debug_redacts_handler() { + struct StubHandler; + #[async_trait] + impl ListModelsHandler for StubHandler { + async fn list_models(&self) -> Result> { + Ok(vec![]) + } + } + let opts = ClientOptions { + on_list_models: Some(Arc::new(StubHandler)), + github_token: Some("secret-token".into()), + ..Default::default() + }; + let debug = format!("{opts:?}"); + assert!(debug.contains("on_list_models: Some(\"\")")); + assert!(debug.contains("github_token: Some(\"\")")); + assert!(!debug.contains("secret-token")); +} + +#[tokio::test] +async fn list_models_uses_on_list_models_handler_when_set() { + use std::sync::atomic::{AtomicUsize, Ordering}; + + struct CountingHandler { + calls: Arc, + models: Vec, + } + #[async_trait] + impl ListModelsHandler for CountingHandler { + async fn list_models(&self) -> Result> { + self.calls.fetch_add(1, Ordering::SeqCst); + Ok(self.models.clone()) + } + } + + let calls = Arc::new(AtomicUsize::new(0)); + let model = Model { + id: "byok-gpt-4".into(), + name: "BYOK GPT-4".into(), + ..Default::default() + }; + let handler: Arc = Arc::new(CountingHandler { + calls: Arc::clone(&calls), + models: vec![model.clone()], + }); + + let client = client_with_list_models_handler(handler); + + let result = client.list_models().await.unwrap(); + assert_eq!(result.len(), 1); + assert_eq!(result[0].id, "byok-gpt-4"); + assert_eq!(calls.load(Ordering::SeqCst), 1); +} + +#[tokio::test] +async fn list_models_serializes_concurrent_cache_misses() { + use std::sync::atomic::{AtomicUsize, Ordering}; + + struct SlowCountingHandler { + calls: Arc, + models: Vec, + } + #[async_trait] + impl ListModelsHandler for SlowCountingHandler { + async fn list_models(&self) -> Result> { + self.calls.fetch_add(1, Ordering::SeqCst); + tokio::time::sleep(std::time::Duration::from_millis(25)).await; + Ok(self.models.clone()) + } + } + + let calls = Arc::new(AtomicUsize::new(0)); + let model = Model { + id: "single-flight-model".into(), + name: "Single Flight Model".into(), + ..Default::default() + }; + let handler: Arc = Arc::new(SlowCountingHandler { + calls: Arc::clone(&calls), + models: vec![model], + }); + let client = client_with_list_models_handler(handler); + + let (first, second) = tokio::join!(client.list_models(), client.list_models()); + assert_eq!(first.unwrap()[0].id, "single-flight-model"); + assert_eq!(second.unwrap()[0].id, "single-flight-model"); + assert_eq!(calls.load(Ordering::SeqCst), 1); +} + +#[tokio::test] +async fn cancelled_resume_session_unregisters_pending_session() { + let (client_write, _server_read) = tokio::io::duplex(8192); + let (_server_write, client_read) = tokio::io::duplex(8192); + let client = Client::from_streams(client_read, client_write, std::env::temp_dir()).unwrap(); + assert!(client.startup_timings().is_none()); + let session_id = SessionId::new("resume-cancel-test"); + let handle = tokio::spawn({ + let client = client.clone(); + async move { + client + .resume_session(ResumeSessionConfig::new(session_id)) + .await + } + }); + + wait_for_pending_session_registration(&client).await; + handle.abort(); + let _ = handle.await; + + assert!(client.inner.router.session_ids().is_empty()); + client.force_stop(); +} + +#[cfg(any(unix, windows))] +#[tokio::test] +async fn dropping_last_client_kills_spawned_cli() { + let temp = tempfile::tempdir().unwrap(); + let ready = temp.path().join("ready"); + let survived = temp.path().join("survived"); + let child = test_child_command(temp.path(), &ready, &survived) + .spawn() + .unwrap(); + let (client_write, _server_read) = tokio::io::duplex(64); + let (_server_write, client_read) = tokio::io::duplex(64); + let client = Client::from_transport( + client_read, + client_write, + Some(child), + None, + temp.path().to_path_buf(), + None, + None, + false, + false, + false, + None, + None, + None, + ClientMode::default(), + None, + false, + ) + .unwrap(); + + wait_for_test_child(&ready).await; + drop(client); + + assert_test_child_killed(&survived).await; +} + +#[cfg(any(unix, windows))] +#[tokio::test] +async fn spawned_child_is_killed_when_dropped() { + let temp = tempfile::tempdir().unwrap(); + let ready = temp.path().join("ready"); + let survived = temp.path().join("survived"); + let child = test_child_command(temp.path(), &ready, &survived) + .spawn() + .unwrap(); + + wait_for_test_child(&ready).await; + drop(child); + + assert_test_child_killed(&survived).await; +} + +#[cfg(any(unix, windows))] +fn test_child_command(temp: &Path, ready: &Path, survived: &Path) -> Command { + let mut command = Client::build_command(Path::new("node"), &ClientOptions::default(), temp); + #[cfg(windows)] + { + const CREATE_NO_WINDOW: u32 = 0x0800_0000; + command.creation_flags(CREATE_NO_WINDOW); + } + command + .args([ + "-e", + r#" + const fs = require("node:fs"); + fs.writeFileSync(process.env.READY, "ready"); + setTimeout(() => fs.writeFileSync(process.env.SURVIVED, "survived"), 1000); + "#, + ]) + .env("READY", ready) + .env("SURVIVED", survived) + .stderr(Stdio::inherit()); + command +} + +#[cfg(any(unix, windows))] +async fn wait_for_test_child(ready: &Path) { + let deadline = tokio::time::Instant::now() + Duration::from_secs(30); + while !ready.exists() { + assert!( + tokio::time::Instant::now() < deadline, + "child did not report readiness" + ); + tokio::time::sleep(Duration::from_millis(10)).await; + } +} + +#[cfg(any(unix, windows))] +async fn assert_test_child_killed(survived: &Path) { + tokio::time::sleep(Duration::from_millis(1500)).await; + + assert!( + !survived.exists(), + "child survived after its owner was dropped" + ); +} + +fn client_with_list_models_handler(handler: Arc) -> Client { + Client { + ahp_host_sessions: None, + inner: Arc::new(ClientInner { + ahp_host_callbacks: Arc::new(ahp_host::ExitCallbacks::default()), + ahp_host_sessions: std::sync::Weak::new(), + child: parking_lot::Mutex::new(None), + owns_stdio: false, + force_stop_requested: tokio_util::sync::CancellationToken::new(), + process_tree: parking_lot::Mutex::new(None), + #[cfg(feature = "in-process")] + ffi_host: parking_lot::Mutex::new(None), + rpc: { + let (req_tx, _req_rx) = mpsc::unbounded_channel(); + let (notif_tx, _notif_rx) = broadcast::channel(16); + let (read_pipe, _write_pipe) = tokio::io::duplex(64); + let (_unused_read, write_pipe) = tokio::io::duplex(64); + JsonRpcClient::new(write_pipe, read_pipe, notif_tx, req_tx) + }, + cwd: PathBuf::from("."), + request_rx: parking_lot::Mutex::new(None), + notification_tx: broadcast::channel(16).0, + router: router::SessionRouter::new(), + github_token_registry: Arc::new(github_token::GitHubTokenRegistry::new()), + negotiated_protocol_version: OnceLock::new(), + state: parking_lot::Mutex::new(ConnectionState::Connected), + lifecycle_tx: broadcast::channel(16).0, + on_list_models: Some(handler), + models_cache: parking_lot::Mutex::new(Arc::new(tokio::sync::OnceCell::new())), + session_fs_configured: false, + session_fs_sqlite_declared: false, + session_fs_binary_declared: false, + llm_inference: OnceLock::new(), + extension_launch_provider: Arc::new( + extension_launch_provider::ExtensionLaunchProviderDispatcher::new(None), + ), + installation_confirmation: Arc::new( + installation_confirmation::InstallationConfirmationDispatcher::new(), + ), + on_github_telemetry: None, + on_get_trace_context: None, + effective_connection_token: None, + mode: ClientMode::default(), + client_info: None, + startup_timings: OnceLock::new(), + }), + } +} + +async fn wait_for_pending_session_registration(client: &Client) { + let deadline = tokio::time::Instant::now() + std::time::Duration::from_secs(1); + while client.inner.router.session_ids().is_empty() { + assert!( + tokio::time::Instant::now() < deadline, + "session was not registered" + ); + tokio::time::sleep(std::time::Duration::from_millis(10)).await; + } +} diff --git a/rust/src/tool.rs b/rust/src/tool.rs index 344d2894ca..12274dbb8b 100644 --- a/rust/src/tool.rs +++ b/rust/src/tool.rs @@ -363,423 +363,4 @@ where } #[cfg(test)] -mod tests { - use super::*; - use crate::types::SessionId; - - struct EchoTool; - - fn echo_tool() -> Tool { - Tool { - name: "echo".to_string(), - description: "Echo the input".to_string(), - parameters: tool_parameters(serde_json::json!({"type": "object"})), - ..Default::default() - } - .with_handler(std::sync::Arc::new(EchoTool)) - } - - #[async_trait] - impl ToolHandler for EchoTool { - async fn call(&self, inv: ToolInvocation) -> Result { - Ok(ToolResult::Text(inv.arguments.to_string())) - } - } - - #[test] - fn tool_handler_returns_tool_definition() { - let def = echo_tool(); - assert_eq!(def.name, "echo"); - assert_eq!(def.description, "Echo the input"); - assert!(def.parameters.contains_key("type")); - assert!(def.handler.is_some()); - } - - #[test] - fn try_tool_parameters_rejects_non_object_schema() { - let err = try_tool_parameters(serde_json::json!(["not", "an", "object"])) - .expect_err("non-object schemas should be rejected"); - - assert!(err.is_data()); - } - - #[test] - fn tool_parameters_serialize_in_deterministic_order() { - // Regression: `Tool.parameters` was a `HashMap`, whose per-instance - // random iteration order made the serialized top-level schema keys - // differ between constructions (and between sessions), busting the - // model provider's prompt cache. `IndexMap` keeps the order stable. - let schema = serde_json::json!({ - "type": "object", - "properties": { - "url": { "type": "string" }, - "count": { "type": "integer" } - }, - "required": ["url"], - "additionalProperties": false - }); - - let build = || Tool { - name: "fetch".to_string(), - parameters: tool_parameters(schema.clone()), - ..Default::default() - }; - - let expected = serde_json::to_string(&build()).expect("serialize tool"); - for _ in 0..64 { - let actual = serde_json::to_string(&build()).expect("serialize tool"); - assert_eq!(actual, expected); - } - - // Pin the exact top-level key order so a regression to any - // order-randomizing container is caught, not just internal drift. - let tool = build(); - let keys: Vec<&str> = tool.parameters.keys().map(String::as_str).collect(); - assert_eq!( - keys, - ["additionalProperties", "properties", "required", "type"] - ); - } - - #[test] - fn convert_mcp_call_tool_result_collects_text_and_binary_content() { - let result = convert_mcp_call_tool_result(&serde_json::json!({ - "isError": true, - "content": [ - { "type": "text", "text": "hello" }, - { "type": "image", "data": "aW1n", "mimeType": "image/png" }, - { - "type": "resource", - "resource": { - "uri": "file:///tmp/data.bin", - "blob": "Ymlu", - "mimeType": "application/octet-stream", - "text": "resource text" - } - } - ] - })) - .expect("valid CallToolResult should convert"); - - let ToolResult::Expanded(expanded) = result else { - panic!("expected expanded tool result"); - }; - - assert_eq!(expanded.text_result_for_llm, "hello\nresource text"); - assert_eq!(expanded.result_type, "failure"); - let binary_results = expanded - .binary_results_for_llm - .expect("binary results should be captured"); - assert_eq!(binary_results.len(), 2); - assert_eq!(binary_results[0].r#type, "image"); - assert_eq!(binary_results[0].data, "aW1n"); - assert_eq!(binary_results[0].mime_type, "image/png"); - assert_eq!( - binary_results[1].description.as_deref(), - Some("file:///tmp/data.bin") - ); - } - - #[test] - fn convert_mcp_call_tool_result_converts_image_content() { - let result = convert_mcp_call_tool_result(&serde_json::json!({ - "content": [ - { "type": "image", "data": "aW1hZ2U=", "mimeType": "image/jpeg" } - ] - })) - .expect("valid CallToolResult should convert"); - - let ToolResult::Expanded(expanded) = result else { - panic!("expected expanded tool result"); - }; - - assert_eq!(expanded.text_result_for_llm, ""); - assert_eq!(expanded.result_type, "success"); - let binary_results = expanded - .binary_results_for_llm - .expect("image result should be captured"); - assert_eq!(binary_results.len(), 1); - assert_eq!(binary_results[0].data, "aW1hZ2U="); - assert_eq!(binary_results[0].mime_type, "image/jpeg"); - assert_eq!(binary_results[0].r#type, "image"); - assert!(binary_results[0].description.is_none()); - } - - #[test] - fn convert_mcp_call_tool_result_converts_resource_blob_content() { - let result = convert_mcp_call_tool_result(&serde_json::json!({ - "content": [ - { - "type": "resource", - "resource": { - "uri": "file:///tmp/report.pdf", - "blob": "cGRm", - "mimeType": "application/pdf" - } - } - ] - })) - .expect("valid CallToolResult should convert"); - - let ToolResult::Expanded(expanded) = result else { - panic!("expected expanded tool result"); - }; - - let binary_results = expanded - .binary_results_for_llm - .expect("resource result should be captured"); - assert_eq!(binary_results.len(), 1); - assert_eq!(binary_results[0].data, "cGRm"); - assert_eq!(binary_results[0].mime_type, "application/pdf"); - assert_eq!(binary_results[0].r#type, "resource"); - assert_eq!( - binary_results[0].description.as_deref(), - Some("file:///tmp/report.pdf") - ); - } - - #[test] - fn convert_mcp_call_tool_result_defaults_resource_blob_mime_type() { - let result = convert_mcp_call_tool_result(&serde_json::json!({ - "content": [ - { - "type": "resource", - "resource": { - "uri": "file:///tmp/data.bin", - "blob": "Ymlu" - } - }, - { - "type": "resource", - "resource": { - "blob": "YmluMg==", - "mimeType": "" - } - } - ] - })) - .expect("valid CallToolResult should convert"); - - let ToolResult::Expanded(expanded) = result else { - panic!("expected expanded tool result"); - }; - - let binary_results = expanded - .binary_results_for_llm - .expect("resource blobs should be captured"); - assert_eq!(binary_results.len(), 2); - assert_eq!(binary_results[0].mime_type, "application/octet-stream"); - assert_eq!(binary_results[1].mime_type, "application/octet-stream"); - } - - #[test] - fn convert_mcp_call_tool_result_omits_binary_results_without_binary_content() { - let result = convert_mcp_call_tool_result(&serde_json::json!({ - "content": [ - { "type": "text", "text": "hello" }, - { - "type": "resource", - "resource": { - "uri": "file:///tmp/readme.md", - "text": "resource text" - } - } - ] - })) - .expect("valid CallToolResult should convert"); - - let ToolResult::Expanded(expanded) = result else { - panic!("expected expanded tool result"); - }; - - assert_eq!(expanded.text_result_for_llm, "hello\nresource text"); - assert!(expanded.binary_results_for_llm.is_none()); - } - - #[tokio::test] - async fn tool_handler_call_returns_result() { - let tool = EchoTool; - let inv = ToolInvocation { - session_id: SessionId::from("s1"), - tool_call_id: "tc1".to_string(), - tool_name: "echo".to_string(), - arguments: serde_json::json!({"msg": "hello"}), - available_tools: None, - traceparent: None, - tracestate: None, - }; - - let result = tool.call(inv).await.unwrap(); - match result { - ToolResult::Text(s) => assert!(s.contains("hello")), - _ => panic!("expected Text result"), - } - } - - #[cfg(feature = "derive")] - #[tokio::test] - async fn define_tool_builds_schema_and_dispatches() { - use serde::Deserialize; - - #[derive(Deserialize, schemars::JsonSchema)] - struct Params { - city: String, - } - - let tool = define_tool( - "weather", - "Get the weather for a city", - |_inv, params: Params| async move { - Ok(ToolResult::Text(format!("sunny in {}", params.city))) - }, - ); - - assert_eq!(tool.name, "weather"); - assert_eq!(tool.description, "Get the weather for a city"); - assert_eq!(tool.parameters["type"], "object"); - assert!(tool.parameters["properties"]["city"].is_object()); - let handler = tool.handler.as_ref().expect("define_tool attaches handler"); - - let inv = ToolInvocation { - session_id: SessionId::from("s1"), - tool_call_id: "tc1".to_string(), - tool_name: "weather".to_string(), - arguments: serde_json::json!({"city": "Seattle"}), - available_tools: None, - traceparent: None, - tracestate: None, - }; - match handler.call(inv).await.unwrap() { - ToolResult::Text(s) => assert_eq!(s, "sunny in Seattle"), - _ => panic!("expected Text result"), - } - } - - // Tests requiring `schemars` (the `derive` feature). - #[cfg(feature = "derive")] - mod derive_tests { - use serde::Deserialize; - - use super::super::*; - use crate::{ErrorKind, SessionId}; - - #[derive(Deserialize, schemars::JsonSchema)] - struct GetWeatherParams { - /// City name to get weather for. - city: String, - /// Temperature unit (celsius or fahrenheit). - unit: Option, - } - - #[test] - fn schema_for_generates_clean_schema() { - let schema = schema_for::(); - assert_eq!(schema["type"], "object"); - assert!(schema["properties"]["city"].is_object()); - assert!(schema["properties"]["unit"].is_object()); - // city is required (non-Option), unit is not - let required = schema["required"].as_array().unwrap(); - assert!(required.contains(&serde_json::json!("city"))); - assert!(!required.contains(&serde_json::json!("unit"))); - // Root-level metadata stripped - assert!(schema.get("$schema").is_none()); - assert!(schema.get("title").is_none()); - } - - struct GetWeatherTool; - - fn get_weather_tool() -> Tool { - Tool { - name: "get_weather".to_string(), - description: "Get weather for a city".to_string(), - parameters: tool_parameters(schema_for::()), - ..Default::default() - } - .with_handler(std::sync::Arc::new(GetWeatherTool)) - } - - #[async_trait] - impl ToolHandler for GetWeatherTool { - async fn call(&self, inv: ToolInvocation) -> Result { - let params: GetWeatherParams = serde_json::from_value(inv.arguments)?; - Ok(ToolResult::Text(format!( - "{} {}", - params.city, - params.unit.unwrap_or_default() - ))) - } - } - - #[test] - fn tool_handler_with_schema_for() { - let def = get_weather_tool(); - assert_eq!(def.name, "get_weather"); - let schema = serde_json::to_value(&def.parameters).expect("serialize tool parameters"); - assert_eq!(schema["type"], "object"); - assert!(schema["properties"]["city"].is_object()); - assert!(def.handler.is_some()); - } - - #[tokio::test] - async fn tool_handler_deserializes_typed_params() { - let tool = GetWeatherTool; - let inv = ToolInvocation { - session_id: SessionId::from("s1"), - tool_call_id: "tc1".to_string(), - tool_name: "get_weather".to_string(), - arguments: serde_json::json!({"city": "Seattle", "unit": "celsius"}), - available_tools: None, - traceparent: None, - tracestate: None, - }; - - let result = tool.call(inv).await.unwrap(); - match result { - ToolResult::Text(s) => assert_eq!(s, "Seattle celsius"), - _ => panic!("expected Text result"), - } - } - - #[tokio::test] - async fn tool_handler_returns_error_on_bad_params() { - let tool = GetWeatherTool; - let inv = ToolInvocation { - session_id: SessionId::from("s1"), - tool_call_id: "tc1".to_string(), - tool_name: "get_weather".to_string(), - arguments: serde_json::json!({"wrong_field": 42}), - available_tools: None, - traceparent: None, - tracestate: None, - }; - - let err = tool.call(inv).await.unwrap_err(); - assert!(matches!(err.kind(), ErrorKind::Json)); - } - - #[tokio::test] - async fn schema_for_derived_tool_round_trips_through_call() { - let tool = GetWeatherTool; - - // Calling the tool with matching arguments returns the - // expected typed result. (Per-name dispatch is the SDK's - // concern; here we exercise just the handler contract.) - let result = tool - .call(ToolInvocation { - session_id: SessionId::from("s1"), - tool_call_id: "tc1".to_string(), - tool_name: "get_weather".to_string(), - arguments: serde_json::json!({"city": "Portland"}), - available_tools: None, - traceparent: None, - tracestate: None, - }) - .await - .expect("ToolHandler::call should succeed for matching args"); - match result { - ToolResult::Text(s) => assert!(s.contains("Portland")), - _ => panic!("expected ToolResult::Text"), - } - } - } -} +mod tests; diff --git a/rust/src/tool/tests.rs b/rust/src/tool/tests.rs new file mode 100644 index 0000000000..e9a0192964 --- /dev/null +++ b/rust/src/tool/tests.rs @@ -0,0 +1,422 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +#![cfg(test)] + +use super::*; +use crate::types::SessionId; + +struct EchoTool; + +fn echo_tool() -> Tool { + Tool { + name: "echo".to_string(), + description: "Echo the input".to_string(), + parameters: tool_parameters(serde_json::json!({"type": "object"})), + ..Default::default() + } + .with_handler(std::sync::Arc::new(EchoTool)) +} + +#[async_trait] +impl ToolHandler for EchoTool { + async fn call(&self, inv: ToolInvocation) -> Result { + Ok(ToolResult::Text(inv.arguments.to_string())) + } +} + +#[test] +fn tool_handler_returns_tool_definition() { + let def = echo_tool(); + assert_eq!(def.name, "echo"); + assert_eq!(def.description, "Echo the input"); + assert!(def.parameters.contains_key("type")); + assert!(def.handler.is_some()); +} + +#[test] +fn try_tool_parameters_rejects_non_object_schema() { + let err = try_tool_parameters(serde_json::json!(["not", "an", "object"])) + .expect_err("non-object schemas should be rejected"); + + assert!(err.is_data()); +} + +#[test] +fn tool_parameters_serialize_in_deterministic_order() { + // Regression: `Tool.parameters` was a `HashMap`, whose per-instance + // random iteration order made the serialized top-level schema keys + // differ between constructions (and between sessions), busting the + // model provider's prompt cache. `IndexMap` keeps the order stable. + let schema = serde_json::json!({ + "type": "object", + "properties": { + "url": { "type": "string" }, + "count": { "type": "integer" } + }, + "required": ["url"], + "additionalProperties": false + }); + + let build = || Tool { + name: "fetch".to_string(), + parameters: tool_parameters(schema.clone()), + ..Default::default() + }; + + let expected = serde_json::to_string(&build()).expect("serialize tool"); + for _ in 0..64 { + let actual = serde_json::to_string(&build()).expect("serialize tool"); + assert_eq!(actual, expected); + } + + // Pin the exact top-level key order so a regression to any + // order-randomizing container is caught, not just internal drift. + let tool = build(); + let keys: Vec<&str> = tool.parameters.keys().map(String::as_str).collect(); + assert_eq!( + keys, + ["additionalProperties", "properties", "required", "type"] + ); +} + +#[test] +fn convert_mcp_call_tool_result_collects_text_and_binary_content() { + let result = convert_mcp_call_tool_result(&serde_json::json!({ + "isError": true, + "content": [ + { "type": "text", "text": "hello" }, + { "type": "image", "data": "aW1n", "mimeType": "image/png" }, + { + "type": "resource", + "resource": { + "uri": "file:///tmp/data.bin", + "blob": "Ymlu", + "mimeType": "application/octet-stream", + "text": "resource text" + } + } + ] + })) + .expect("valid CallToolResult should convert"); + + let ToolResult::Expanded(expanded) = result else { + panic!("expected expanded tool result"); + }; + + assert_eq!(expanded.text_result_for_llm, "hello\nresource text"); + assert_eq!(expanded.result_type, "failure"); + let binary_results = expanded + .binary_results_for_llm + .expect("binary results should be captured"); + assert_eq!(binary_results.len(), 2); + assert_eq!(binary_results[0].r#type, "image"); + assert_eq!(binary_results[0].data, "aW1n"); + assert_eq!(binary_results[0].mime_type, "image/png"); + assert_eq!( + binary_results[1].description.as_deref(), + Some("file:///tmp/data.bin") + ); +} + +#[test] +fn convert_mcp_call_tool_result_converts_image_content() { + let result = convert_mcp_call_tool_result(&serde_json::json!({ + "content": [ + { "type": "image", "data": "aW1hZ2U=", "mimeType": "image/jpeg" } + ] + })) + .expect("valid CallToolResult should convert"); + + let ToolResult::Expanded(expanded) = result else { + panic!("expected expanded tool result"); + }; + + assert_eq!(expanded.text_result_for_llm, ""); + assert_eq!(expanded.result_type, "success"); + let binary_results = expanded + .binary_results_for_llm + .expect("image result should be captured"); + assert_eq!(binary_results.len(), 1); + assert_eq!(binary_results[0].data, "aW1hZ2U="); + assert_eq!(binary_results[0].mime_type, "image/jpeg"); + assert_eq!(binary_results[0].r#type, "image"); + assert!(binary_results[0].description.is_none()); +} + +#[test] +fn convert_mcp_call_tool_result_converts_resource_blob_content() { + let result = convert_mcp_call_tool_result(&serde_json::json!({ + "content": [ + { + "type": "resource", + "resource": { + "uri": "file:///tmp/report.pdf", + "blob": "cGRm", + "mimeType": "application/pdf" + } + } + ] + })) + .expect("valid CallToolResult should convert"); + + let ToolResult::Expanded(expanded) = result else { + panic!("expected expanded tool result"); + }; + + let binary_results = expanded + .binary_results_for_llm + .expect("resource result should be captured"); + assert_eq!(binary_results.len(), 1); + assert_eq!(binary_results[0].data, "cGRm"); + assert_eq!(binary_results[0].mime_type, "application/pdf"); + assert_eq!(binary_results[0].r#type, "resource"); + assert_eq!( + binary_results[0].description.as_deref(), + Some("file:///tmp/report.pdf") + ); +} + +#[test] +fn convert_mcp_call_tool_result_defaults_resource_blob_mime_type() { + let result = convert_mcp_call_tool_result(&serde_json::json!({ + "content": [ + { + "type": "resource", + "resource": { + "uri": "file:///tmp/data.bin", + "blob": "Ymlu" + } + }, + { + "type": "resource", + "resource": { + "blob": "YmluMg==", + "mimeType": "" + } + } + ] + })) + .expect("valid CallToolResult should convert"); + + let ToolResult::Expanded(expanded) = result else { + panic!("expected expanded tool result"); + }; + + let binary_results = expanded + .binary_results_for_llm + .expect("resource blobs should be captured"); + assert_eq!(binary_results.len(), 2); + assert_eq!(binary_results[0].mime_type, "application/octet-stream"); + assert_eq!(binary_results[1].mime_type, "application/octet-stream"); +} + +#[test] +fn convert_mcp_call_tool_result_omits_binary_results_without_binary_content() { + let result = convert_mcp_call_tool_result(&serde_json::json!({ + "content": [ + { "type": "text", "text": "hello" }, + { + "type": "resource", + "resource": { + "uri": "file:///tmp/readme.md", + "text": "resource text" + } + } + ] + })) + .expect("valid CallToolResult should convert"); + + let ToolResult::Expanded(expanded) = result else { + panic!("expected expanded tool result"); + }; + + assert_eq!(expanded.text_result_for_llm, "hello\nresource text"); + assert!(expanded.binary_results_for_llm.is_none()); +} + +#[tokio::test] +async fn tool_handler_call_returns_result() { + let tool = EchoTool; + let inv = ToolInvocation { + session_id: SessionId::from("s1"), + tool_call_id: "tc1".to_string(), + tool_name: "echo".to_string(), + arguments: serde_json::json!({"msg": "hello"}), + available_tools: None, + traceparent: None, + tracestate: None, + }; + + let result = tool.call(inv).await.unwrap(); + match result { + ToolResult::Text(s) => assert!(s.contains("hello")), + _ => panic!("expected Text result"), + } +} + +#[cfg(feature = "derive")] +#[tokio::test] +async fn define_tool_builds_schema_and_dispatches() { + use serde::Deserialize; + + #[derive(Deserialize, schemars::JsonSchema)] + struct Params { + city: String, + } + + let tool = define_tool( + "weather", + "Get the weather for a city", + |_inv, params: Params| async move { Ok(ToolResult::Text(format!("sunny in {}", params.city))) }, + ); + + assert_eq!(tool.name, "weather"); + assert_eq!(tool.description, "Get the weather for a city"); + assert_eq!(tool.parameters["type"], "object"); + assert!(tool.parameters["properties"]["city"].is_object()); + let handler = tool.handler.as_ref().expect("define_tool attaches handler"); + + let inv = ToolInvocation { + session_id: SessionId::from("s1"), + tool_call_id: "tc1".to_string(), + tool_name: "weather".to_string(), + arguments: serde_json::json!({"city": "Seattle"}), + available_tools: None, + traceparent: None, + tracestate: None, + }; + match handler.call(inv).await.unwrap() { + ToolResult::Text(s) => assert_eq!(s, "sunny in Seattle"), + _ => panic!("expected Text result"), + } +} + +// Tests requiring `schemars` (the `derive` feature). +#[cfg(feature = "derive")] +mod derive_tests { + use serde::Deserialize; + + use super::super::*; + use crate::{ErrorKind, SessionId}; + + #[derive(Deserialize, schemars::JsonSchema)] + struct GetWeatherParams { + /// City name to get weather for. + city: String, + /// Temperature unit (celsius or fahrenheit). + unit: Option, + } + + #[test] + fn schema_for_generates_clean_schema() { + let schema = schema_for::(); + assert_eq!(schema["type"], "object"); + assert!(schema["properties"]["city"].is_object()); + assert!(schema["properties"]["unit"].is_object()); + // city is required (non-Option), unit is not + let required = schema["required"].as_array().unwrap(); + assert!(required.contains(&serde_json::json!("city"))); + assert!(!required.contains(&serde_json::json!("unit"))); + // Root-level metadata stripped + assert!(schema.get("$schema").is_none()); + assert!(schema.get("title").is_none()); + } + + struct GetWeatherTool; + + fn get_weather_tool() -> Tool { + Tool { + name: "get_weather".to_string(), + description: "Get weather for a city".to_string(), + parameters: tool_parameters(schema_for::()), + ..Default::default() + } + .with_handler(std::sync::Arc::new(GetWeatherTool)) + } + + #[async_trait] + impl ToolHandler for GetWeatherTool { + async fn call(&self, inv: ToolInvocation) -> Result { + let params: GetWeatherParams = serde_json::from_value(inv.arguments)?; + Ok(ToolResult::Text(format!( + "{} {}", + params.city, + params.unit.unwrap_or_default() + ))) + } + } + + #[test] + fn tool_handler_with_schema_for() { + let def = get_weather_tool(); + assert_eq!(def.name, "get_weather"); + let schema = serde_json::to_value(&def.parameters).expect("serialize tool parameters"); + assert_eq!(schema["type"], "object"); + assert!(schema["properties"]["city"].is_object()); + assert!(def.handler.is_some()); + } + + #[tokio::test] + async fn tool_handler_deserializes_typed_params() { + let tool = GetWeatherTool; + let inv = ToolInvocation { + session_id: SessionId::from("s1"), + tool_call_id: "tc1".to_string(), + tool_name: "get_weather".to_string(), + arguments: serde_json::json!({"city": "Seattle", "unit": "celsius"}), + available_tools: None, + traceparent: None, + tracestate: None, + }; + + let result = tool.call(inv).await.unwrap(); + match result { + ToolResult::Text(s) => assert_eq!(s, "Seattle celsius"), + _ => panic!("expected Text result"), + } + } + + #[tokio::test] + async fn tool_handler_returns_error_on_bad_params() { + let tool = GetWeatherTool; + let inv = ToolInvocation { + session_id: SessionId::from("s1"), + tool_call_id: "tc1".to_string(), + tool_name: "get_weather".to_string(), + arguments: serde_json::json!({"wrong_field": 42}), + available_tools: None, + traceparent: None, + tracestate: None, + }; + + let err = tool.call(inv).await.unwrap_err(); + assert!(matches!(err.kind(), ErrorKind::Json)); + } + + #[tokio::test] + async fn schema_for_derived_tool_round_trips_through_call() { + let tool = GetWeatherTool; + + // Calling the tool with matching arguments returns the + // expected typed result. (Per-name dispatch is the SDK's + // concern; here we exercise just the handler contract.) + let result = tool + .call(ToolInvocation { + session_id: SessionId::from("s1"), + tool_call_id: "tc1".to_string(), + tool_name: "get_weather".to_string(), + arguments: serde_json::json!({"city": "Portland"}), + available_tools: None, + traceparent: None, + tracestate: None, + }) + .await + .expect("ToolHandler::call should succeed for matching args"); + match result { + ToolResult::Text(s) => assert!(s.contains("Portland")), + _ => panic!("expected ToolResult::Text"), + } + } +} diff --git a/rust/src/trace_context.rs b/rust/src/trace_context.rs index 287c87cbd3..7344e07167 100644 --- a/rust/src/trace_context.rs +++ b/rust/src/trace_context.rs @@ -102,31 +102,4 @@ pub(crate) fn inject_trace_context(params: &mut serde_json::Value, ctx: &TraceCo } #[cfg(test)] -mod tests { - use super::TraceContext; - - #[test] - fn new_yields_empty_context() { - let ctx = TraceContext::new(); - assert!(ctx.is_empty()); - assert!(ctx.traceparent.is_none()); - assert!(ctx.tracestate.is_none()); - } - - #[test] - fn builder_composes_traceparent_and_tracestate() { - let ctx = TraceContext::new() - .with_traceparent("00-trace-span-01") - .with_tracestate("vendor=key"); - assert_eq!(ctx.traceparent.as_deref(), Some("00-trace-span-01")); - assert_eq!(ctx.tracestate.as_deref(), Some("vendor=key")); - assert!(!ctx.is_empty()); - } - - #[test] - fn from_traceparent_matches_builder() { - let direct = TraceContext::from_traceparent("00-trace-span-01"); - let chained = TraceContext::new().with_traceparent("00-trace-span-01"); - assert_eq!(direct, chained); - } -} +mod tests; diff --git a/rust/src/trace_context/tests.rs b/rust/src/trace_context/tests.rs new file mode 100644 index 0000000000..ff151b80cb --- /dev/null +++ b/rust/src/trace_context/tests.rs @@ -0,0 +1,32 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +#![cfg(test)] + +use super::TraceContext; + +#[test] +fn new_yields_empty_context() { + let ctx = TraceContext::new(); + assert!(ctx.is_empty()); + assert!(ctx.traceparent.is_none()); + assert!(ctx.tracestate.is_none()); +} + +#[test] +fn builder_composes_traceparent_and_tracestate() { + let ctx = TraceContext::new() + .with_traceparent("00-trace-span-01") + .with_tracestate("vendor=key"); + assert_eq!(ctx.traceparent.as_deref(), Some("00-trace-span-01")); + assert_eq!(ctx.tracestate.as_deref(), Some("vendor=key")); + assert!(!ctx.is_empty()); +} + +#[test] +fn from_traceparent_matches_builder() { + let direct = TraceContext::from_traceparent("00-trace-span-01"); + let chained = TraceContext::new().with_traceparent("00-trace-span-01"); + assert_eq!(direct, chained); +} diff --git a/rust/src/transforms.rs b/rust/src/transforms.rs index a090bc6494..dc93094c25 100644 --- a/rust/src/transforms.rs +++ b/rust/src/transforms.rs @@ -110,114 +110,4 @@ pub(crate) async fn dispatch_transform( } #[cfg(test)] -mod tests { - use super::*; - - struct TestTransform; - - #[async_trait] - impl SystemMessageTransform for TestTransform { - fn section_ids(&self) -> Vec { - vec!["instructions".to_string(), "context".to_string()] - } - - async fn transform_section( - &self, - section_id: &str, - content: &str, - _ctx: TransformContext, - ) -> Option { - match section_id { - "instructions" => Some(format!("[modified] {content}")), - _ => None, - } - } - } - - #[tokio::test] - async fn dispatch_applies_matching_transform() { - let transform = TestTransform; - let mut sections = HashMap::new(); - sections.insert( - "instructions".to_string(), - TransformSection { - content: "be helpful".to_string(), - }, - ); - - let response = dispatch_transform(&transform, &SessionId::new("sess-1"), sections).await; - assert_eq!( - response.sections["instructions"].content, - "[modified] be helpful" - ); - } - - #[tokio::test] - async fn dispatch_passes_through_unhandled_section() { - let transform = TestTransform; - let mut sections = HashMap::new(); - sections.insert( - "context".to_string(), - TransformSection { - content: "original context".to_string(), - }, - ); - - let response = dispatch_transform(&transform, &SessionId::new("sess-1"), sections).await; - assert_eq!(response.sections["context"].content, "original context"); - } - - #[tokio::test] - async fn dispatch_unknown_section_passes_through() { - let transform = TestTransform; - let mut sections = HashMap::new(); - sections.insert( - "unknown".to_string(), - TransformSection { - content: "mystery".to_string(), - }, - ); - - let response = dispatch_transform(&transform, &SessionId::new("sess-1"), sections).await; - assert_eq!(response.sections["unknown"].content, "mystery"); - } - - #[tokio::test] - async fn dispatch_mixed_sections() { - let transform = TestTransform; - let mut sections = HashMap::new(); - sections.insert( - "instructions".to_string(), - TransformSection { - content: "help me".to_string(), - }, - ); - sections.insert( - "context".to_string(), - TransformSection { - content: "some context".to_string(), - }, - ); - sections.insert( - "other".to_string(), - TransformSection { - content: "other stuff".to_string(), - }, - ); - - let response = dispatch_transform(&transform, &SessionId::new("sess-1"), sections).await; - assert_eq!( - response.sections["instructions"].content, - "[modified] help me" - ); - assert_eq!(response.sections["context"].content, "some context"); - assert_eq!(response.sections["other"].content, "other stuff"); - } - - #[tokio::test] - async fn section_ids_returns_registered_sections() { - let transform = TestTransform; - let ids = transform.section_ids(); - assert_eq!(ids, vec!["instructions", "context"]); - } -} +mod tests; diff --git a/rust/src/transforms/tests.rs b/rust/src/transforms/tests.rs new file mode 100644 index 0000000000..c56f804920 --- /dev/null +++ b/rust/src/transforms/tests.rs @@ -0,0 +1,115 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +#![cfg(test)] + +use super::*; + +struct TestTransform; + +#[async_trait] +impl SystemMessageTransform for TestTransform { + fn section_ids(&self) -> Vec { + vec!["instructions".to_string(), "context".to_string()] + } + + async fn transform_section( + &self, + section_id: &str, + content: &str, + _ctx: TransformContext, + ) -> Option { + match section_id { + "instructions" => Some(format!("[modified] {content}")), + _ => None, + } + } +} + +#[tokio::test] +async fn dispatch_applies_matching_transform() { + let transform = TestTransform; + let mut sections = HashMap::new(); + sections.insert( + "instructions".to_string(), + TransformSection { + content: "be helpful".to_string(), + }, + ); + + let response = dispatch_transform(&transform, &SessionId::new("sess-1"), sections).await; + assert_eq!( + response.sections["instructions"].content, + "[modified] be helpful" + ); +} + +#[tokio::test] +async fn dispatch_passes_through_unhandled_section() { + let transform = TestTransform; + let mut sections = HashMap::new(); + sections.insert( + "context".to_string(), + TransformSection { + content: "original context".to_string(), + }, + ); + + let response = dispatch_transform(&transform, &SessionId::new("sess-1"), sections).await; + assert_eq!(response.sections["context"].content, "original context"); +} + +#[tokio::test] +async fn dispatch_unknown_section_passes_through() { + let transform = TestTransform; + let mut sections = HashMap::new(); + sections.insert( + "unknown".to_string(), + TransformSection { + content: "mystery".to_string(), + }, + ); + + let response = dispatch_transform(&transform, &SessionId::new("sess-1"), sections).await; + assert_eq!(response.sections["unknown"].content, "mystery"); +} + +#[tokio::test] +async fn dispatch_mixed_sections() { + let transform = TestTransform; + let mut sections = HashMap::new(); + sections.insert( + "instructions".to_string(), + TransformSection { + content: "help me".to_string(), + }, + ); + sections.insert( + "context".to_string(), + TransformSection { + content: "some context".to_string(), + }, + ); + sections.insert( + "other".to_string(), + TransformSection { + content: "other stuff".to_string(), + }, + ); + + let response = dispatch_transform(&transform, &SessionId::new("sess-1"), sections).await; + assert_eq!( + response.sections["instructions"].content, + "[modified] help me" + ); + assert_eq!(response.sections["context"].content, "some context"); + assert_eq!(response.sections["other"].content, "other stuff"); +} + +#[tokio::test] +async fn section_ids_returns_registered_sections() { + let transform = TestTransform; + let ids = transform.section_ids(); + assert_eq!(ids, vec!["instructions", "context"]); +} diff --git a/rust/src/types.rs b/rust/src/types.rs index b0aa884609..dbf40ce4f5 100644 --- a/rust/src/types.rs +++ b/rust/src/types.rs @@ -43,9 +43,9 @@ use crate::handler::{ use crate::hooks::SessionHooks; use crate::provider_token::BearerTokenProvider; pub use crate::session_fs::{ - DirEntry, DirEntryKind, FileInfo, FsError, SessionFsCapabilities, SessionFsConfig, - SessionFsConventions, SessionFsProvider, SessionFsSqliteProvider, SessionFsSqliteQueryResult, - SessionFsSqliteQueryType, SessionFsSqliteTransactionError, + DirEntry, DirEntryKind, FileInfo, FsError, SessionFsBinaryProvider, SessionFsCapabilities, + SessionFsConfig, SessionFsConventions, SessionFsProvider, SessionFsSqliteProvider, + SessionFsSqliteQueryResult, SessionFsSqliteQueryType, SessionFsSqliteTransactionError, SessionFsSqliteTransactionErrorClass, SessionFsSqliteTransactionStatement, }; pub use crate::trace_context::{TraceContext, TraceContextProvider}; @@ -6414,2243 +6414,13 @@ impl Default for ExitPlanModeData { } #[cfg(test)] -mod tests { - use std::collections::HashMap; - use std::path::PathBuf; - - use serde_json::json; - - use super::{ - AgentMode, Attachment, AttachmentLineRange, AttachmentSelectionPosition, - AttachmentSelectionRange, AutoTier, AzureProviderOptions, CapiSessionOptions, - ConnectionState, CopilotExpAssignmentResponse, CustomAgentConfig, DeliveryMode, - ExpConfigEntry, ExpFlagValue, ExtensionInfo, GitHubMcpToolConfig, GitHubReferenceType, - InfiniteSessionConfig, LargeToolOutputConfig, McpServerConfig, McpStdioServerConfig, - MemoryConfiguration, NamedProviderConfig, PermissionResponseCapability, ProviderConfig, - ProviderModelConfig, ReasoningSummary, ResumeSessionConfig, SessionConfig, SessionEvent, - SessionId, SystemMessageConfig, Tool, ToolBinaryResult, ToolResult, ToolResultExpanded, - ToolResultResponse, ensure_attachment_display_names, - }; - use crate::generated::session_events::TypedSessionEvent; - - #[test] - fn permission_response_capability_is_publicly_exported() { - assert_eq!( - serde_json::to_value(PermissionResponseCapability::Interactive).unwrap(), - json!("interactive") - ); - } - - #[test] - fn tool_builder_composes() { - let tool = Tool::new("greet") - .with_description("Say hello") - .with_namespaced_name("hello/greet") - .with_instructions("Pass the user's name") - .with_parameters(json!({ - "type": "object", - "properties": { "name": { "type": "string" } }, - "required": ["name"] - })) - .with_overrides_built_in_tool(true) - .with_skip_permission(true); - assert_eq!(tool.name, "greet"); - assert_eq!(tool.description, "Say hello"); - assert_eq!(tool.namespaced_name.as_deref(), Some("hello/greet")); - assert_eq!(tool.instructions.as_deref(), Some("Pass the user's name")); - assert_eq!(tool.parameters.get("type").unwrap(), &json!("object")); - assert!(tool.overrides_built_in_tool); - assert!(tool.skip_permission); - } - - #[test] - fn tool_defer_serialization() { - let tool = Tool::new("lookup").with_defer(super::DeferMode::Auto); - assert_eq!(tool.defer, Some(super::DeferMode::Auto)); - let value = serde_json::to_value(&tool).unwrap(); - assert_eq!(value.get("defer").unwrap(), &json!("auto")); - - let plain = Tool::new("plain"); - let value = serde_json::to_value(&plain).unwrap(); - assert!(value.get("defer").is_none()); - } - - #[test] - fn tool_metadata_serialization() { - use indexmap::IndexMap; - - let mut metadata = IndexMap::new(); - metadata.insert( - "github.com/copilot:safeForTelemetry".to_string(), - json!({ "name": true, "inputsNames": false }), - ); - let tool = Tool::new("lookup").with_metadata(metadata); - let value = serde_json::to_value(&tool).unwrap(); - assert_eq!( - value - .get("metadata") - .unwrap() - .get("github.com/copilot:safeForTelemetry") - .unwrap(), - &json!({ "name": true, "inputsNames": false }) - ); - - // Empty metadata is omitted on the wire. - let plain = Tool::new("plain"); - let value = serde_json::to_value(&plain).unwrap(); - assert!(value.get("metadata").is_none()); - } - - #[test] - fn custom_agent_config_builder_with_model() { - let agent = CustomAgentConfig::new("my-agent", "You are helpful.") - .with_model("claude-haiku-4.5") - .with_display_name("My Agent"); - assert_eq!(agent.name, "my-agent"); - assert_eq!(agent.model.as_deref(), Some("claude-haiku-4.5")); - assert_eq!(agent.display_name.as_deref(), Some("My Agent")); - } - - #[test] - fn custom_agent_config_serializes_model() { - let agent = CustomAgentConfig::new("model-agent", "prompt").with_model("claude-haiku-4.5"); - let wire = serde_json::to_value(&agent).unwrap(); - assert_eq!(wire["model"], "claude-haiku-4.5"); - assert_eq!(wire["name"], "model-agent"); - } - - #[test] - fn custom_agent_config_omits_model_when_none() { - let agent = CustomAgentConfig::new("no-model-agent", "prompt"); - let wire = serde_json::to_value(&agent).unwrap(); - assert!(wire.get("model").is_none()); - } - - #[test] - fn custom_agent_config_builder_with_reasoning_effort() { - let agent = - CustomAgentConfig::new("reasoning-agent", "prompt").with_reasoning_effort("high"); - assert_eq!(agent.reasoning_effort.as_deref(), Some("high")); - } - - #[test] - fn custom_agent_config_serializes_reasoning_effort() { - let agent = - CustomAgentConfig::new("reasoning-agent", "prompt").with_reasoning_effort("high"); - let wire = serde_json::to_value(&agent).unwrap(); - assert_eq!(wire["reasoningEffort"], "high"); - } - - #[test] - fn custom_agent_config_omits_reasoning_effort_when_none() { - let agent = CustomAgentConfig::new("default-agent", "prompt"); - let wire = serde_json::to_value(&agent).unwrap(); - assert!(wire.get("reasoningEffort").is_none()); - } - - #[test] - #[should_panic(expected = "tool parameter schema must be a JSON object")] - fn tool_with_parameters_panics_on_non_object_value() { - let _ = Tool::new("noop").with_parameters(json!(null)); - } - - #[test] - fn tool_result_expanded_serializes_binary_results_for_llm() { - let response = ToolResultResponse { - result: ToolResult::Expanded(ToolResultExpanded { - text_result_for_llm: "rendered chart".to_string(), - result_type: "success".to_string(), - binary_results_for_llm: Some(vec![ToolBinaryResult { - data: "aW1n".to_string(), - mime_type: "image/png".to_string(), - r#type: "image".to_string(), - description: Some("chart preview".to_string()), - }]), - session_log: None, - error: None, - tool_telemetry: None, - tool_references: None, - }), - }; - - let wire = serde_json::to_value(&response).unwrap(); - - assert_eq!( - wire, - json!({ - "result": { - "textResultForLlm": "rendered chart", - "resultType": "success", - "binaryResultsForLlm": [ - { - "data": "aW1n", - "mimeType": "image/png", - "type": "image", - "description": "chart preview" - } - ] - } - }) - ); - } - - #[test] - fn tool_result_expanded_omits_binary_results_for_llm_when_none() { - let response = ToolResultResponse { - result: ToolResult::Expanded(ToolResultExpanded { - text_result_for_llm: "ok".to_string(), - result_type: "success".to_string(), - binary_results_for_llm: None, - session_log: None, - error: None, - tool_telemetry: None, - tool_references: None, - }), - }; - - let wire = serde_json::to_value(&response).unwrap(); - - assert_eq!(wire["result"]["textResultForLlm"], "ok"); - assert!(wire["result"].get("binaryResultsForLlm").is_none()); - } - - #[test] - fn tool_result_expanded_serializes_tool_references() { - let response = ToolResultResponse { - result: ToolResult::Expanded( - ToolResultExpanded::new("found 2 tools", "success") - .with_tool_references(["get_weather", "check_status"]), - ), - }; - - let wire = serde_json::to_value(&response).unwrap(); - - assert_eq!( - wire, - json!({ - "result": { - "textResultForLlm": "found 2 tools", - "resultType": "success", - "toolReferences": ["get_weather", "check_status"] - } - }) - ); - } - - #[test] - fn tool_result_expanded_omits_tool_references_when_none() { - let response = ToolResultResponse { - result: ToolResult::Expanded(ToolResultExpanded::new("ok", "success")), - }; - - let wire = serde_json::to_value(&response).unwrap(); - - assert_eq!(wire["result"]["textResultForLlm"], "ok"); - assert!(wire["result"].get("toolReferences").is_none()); - } - - #[test] - fn tool_result_expanded_with_tool_references_accepts_owned_strings() { - // The builder is generic over `Into`, so an owned `Vec` - // must compile and populate the field just like a `&str` array. - let names: Vec = vec!["alpha".to_string(), "beta".to_string()]; - let expanded = ToolResultExpanded::new("ok", "success").with_tool_references(names); - - assert_eq!( - expanded.tool_references.as_deref(), - Some(["alpha".to_string(), "beta".to_string()].as_slice()) - ); - } - - #[test] - fn tool_result_expanded_deserializes_tool_references() { - let wire = json!({ - "textResultForLlm": "found tools", - "resultType": "success", - "toolReferences": ["alpha", "beta"] - }); - - let expanded: ToolResultExpanded = serde_json::from_value(wire).unwrap(); - - assert_eq!( - expanded.tool_references.as_deref(), - Some(["alpha".to_string(), "beta".to_string()].as_slice()) - ); - } - - #[test] - fn session_config_default_wire_flags_off_without_handlers() { - let cfg = SessionConfig::default(); - assert_eq!(cfg.mcp_oauth_token_storage, None); - assert_eq!(cfg.allowed_models, None); - // Wire flags are derived from handler presence at create_session - // time, not stored on the config. With no handlers installed, every - // request_* flag should serialize as false. - let (wire, _runtime) = cfg - .into_wire(Some(SessionId::from("default-flags"))) - .expect("default config has no duplicate handlers"); - assert!(!wire.request_user_input); - assert!(!wire.request_permission); - assert!(!wire.request_elicitation); - assert!(!wire.request_exit_plan_mode); - assert!(!wire.request_auto_mode_switch); - assert!(!wire.hooks); - assert!(!wire.request_mcp_apps); - let json = serde_json::to_value(&wire).unwrap(); - assert!(json.get("askUserVariant").is_none()); - assert!(json.get("allowedModels").is_none()); - } - - #[test] - fn resume_session_config_new_wire_flags_off_without_handlers() { - let cfg = ResumeSessionConfig::new(SessionId::from("resume-flags")); - assert_eq!(cfg.mcp_oauth_token_storage, None); - assert_eq!(cfg.allowed_models, None); - let (wire, _runtime) = cfg - .into_wire() - .expect("default resume config has no duplicate handlers"); - assert!(!wire.request_user_input); - assert!(!wire.request_permission); - assert!(!wire.request_elicitation); - assert!(!wire.request_exit_plan_mode); - assert!(!wire.request_auto_mode_switch); - assert!(!wire.hooks); - assert!(!wire.request_mcp_apps); - let json = serde_json::to_value(&wire).unwrap(); - assert!(json.get("askUserVariant").is_none()); - assert!(json.get("allowedModels").is_none()); - } - - #[test] - fn session_configs_build_debug_and_serialize_allowed_models() { - let create = SessionConfig::default().with_allowed_models(["gpt-5.4", "claude-sonnet-4"]); - assert_eq!( - create.allowed_models.as_deref(), - Some(&["gpt-5.4".to_string(), "claude-sonnet-4".to_string()][..]) - ); - assert!(format!("{create:?}").contains("allowed_models")); - - let (create_wire, _) = create - .into_wire(Some(SessionId::from("create-allowed-models"))) - .expect("allowed model config has no duplicate handlers"); - let create_json = serde_json::to_value(&create_wire).unwrap(); - assert_eq!( - create_json["allowedModels"], - json!(["gpt-5.4", "claude-sonnet-4"]) - ); - - let resume = ResumeSessionConfig::new(SessionId::from("resume-allowed-models")) - .with_allowed_models(vec!["gpt-5.4".to_string(), "gpt-5-mini".to_string()]); - assert_eq!( - resume.allowed_models.as_deref(), - Some(&["gpt-5.4".to_string(), "gpt-5-mini".to_string()][..]) - ); - assert!(format!("{resume:?}").contains("allowed_models")); - - let (resume_wire, _) = resume - .into_wire() - .expect("resume allowed model config has no duplicate handlers"); - let resume_json = serde_json::to_value(&resume_wire).unwrap(); - assert_eq!( - resume_json["allowedModels"], - json!(["gpt-5.4", "gpt-5-mini"]) - ); - } - - #[test] - fn custom_agents_local_only_serializes_on_create_and_resume() { - let (create_wire, _) = SessionConfig::default() - .with_custom_agents_local_only(false) - .into_wire(Some(SessionId::from("create-locality"))) - .expect("create config has no duplicate handlers"); - let create_json = serde_json::to_value(&create_wire).unwrap(); - assert_eq!(create_json["customAgentsLocalOnly"], false); - - let (resume_wire, _) = ResumeSessionConfig::new(SessionId::from("resume-locality")) - .with_custom_agents_local_only(false) - .into_wire() - .expect("resume config has no duplicate handlers"); - let resume_json = serde_json::to_value(&resume_wire).unwrap(); - assert_eq!(resume_json["customAgentsLocalOnly"], false); - - let (unset_create_wire, _) = SessionConfig::default() - .into_wire(Some(SessionId::from("create-unset"))) - .expect("create config has no duplicate handlers"); - let unset_create_json = serde_json::to_value(&unset_create_wire).unwrap(); - assert!(unset_create_json.get("customAgentsLocalOnly").is_none()); - - let (unset_resume_wire, _) = ResumeSessionConfig::new(SessionId::from("resume-unset")) - .into_wire() - .expect("resume config has no duplicate handlers"); - let unset_resume_json = serde_json::to_value(&unset_resume_wire).unwrap(); - assert!(unset_resume_json.get("customAgentsLocalOnly").is_none()); - } - - #[test] - fn session_config_enable_mcp_apps_sets_wire_flag_and_serializes() { - let cfg = SessionConfig::default().with_enable_mcp_apps(true); - assert_eq!(cfg.enable_mcp_apps, Some(true)); - - let (wire, _runtime) = cfg - .into_wire(Some(SessionId::from("enable-mcp-apps"))) - .expect("enable_mcp_apps config has no duplicate handlers"); - assert!(wire.request_mcp_apps); - - let json = serde_json::to_value(&wire).unwrap(); - assert_eq!(json["requestMcpApps"], serde_json::Value::Bool(true)); - } - - #[test] - fn resume_session_config_enable_mcp_apps_sets_wire_flag_and_serializes() { - let cfg = ResumeSessionConfig::new(SessionId::from("resume-enable-mcp-apps")) - .with_enable_mcp_apps(true); - assert_eq!(cfg.enable_mcp_apps, Some(true)); - - let (wire, _runtime) = cfg - .into_wire() - .expect("resume enable_mcp_apps config has no duplicate handlers"); - assert!(wire.request_mcp_apps); - - let json = serde_json::to_value(&wire).unwrap(); - assert_eq!(json["requestMcpApps"], serde_json::Value::Bool(true)); - } - - #[test] - fn github_mcp_tool_config_serializes_for_create_and_resume() { - let github_config = GitHubMcpToolConfig::new() - .with_enable_all_tools(true) - .with_additional_toolsets(["repos"]) - .with_additional_tools(["get_issue"]) - .with_enable_insiders_mode(true) - .with_disable_form_deferral(true); - - let (create_wire, _) = SessionConfig::default() - .with_github_mcp_tool_config(github_config.clone()) - .into_wire(Some(SessionId::from("github-mcp"))) - .expect("create config has no duplicate handlers"); - assert_eq!( - serde_json::to_value(&create_wire).unwrap()["githubMcpToolConfig"], - serde_json::json!({ - "enableAllTools": true, - "additionalToolsets": ["repos"], - "additionalTools": ["get_issue"], - "enableInsidersMode": true, - "disableFormDeferral": true, - }) - ); - - let (resume_wire, _) = ResumeSessionConfig::new(SessionId::from("github-mcp")) - .with_github_mcp_tool_config(github_config) - .into_wire() - .expect("resume config has no duplicate handlers"); - assert!(resume_wire.github_mcp_tool_config.is_some()); - - let (unset_wire, _) = SessionConfig::default() - .into_wire(Some(SessionId::from("github-mcp-unset"))) - .expect("default config has no duplicate handlers"); - assert!( - serde_json::to_value(&unset_wire) - .unwrap() - .get("githubMcpToolConfig") - .is_none() - ); - } - - #[test] - fn memory_configuration_constructors_and_serde() { - assert!(MemoryConfiguration::enabled().enabled); - assert!(!MemoryConfiguration::disabled().enabled); - assert!(MemoryConfiguration::disabled().with_enabled(true).enabled); - - let json = serde_json::to_value(MemoryConfiguration::enabled()).unwrap(); - assert_eq!(json, serde_json::json!({ "enabled": true })); - } - - #[test] - fn session_config_with_memory_serializes() { - let (wire, _runtime) = SessionConfig::default() - .with_memory(MemoryConfiguration::enabled()) - .into_wire(Some(SessionId::from("memory-on"))) - .expect("no duplicate handlers"); - let json = serde_json::to_value(&wire).unwrap(); - assert_eq!(json["memory"], serde_json::json!({ "enabled": true })); - - let (wire_off, _) = SessionConfig::default() - .with_memory(MemoryConfiguration::disabled()) - .into_wire(Some(SessionId::from("memory-off"))) - .expect("no duplicate handlers"); - let json_off = serde_json::to_value(&wire_off).unwrap(); - assert_eq!(json_off["memory"], serde_json::json!({ "enabled": false })); - - // Unset memory is omitted on the wire. - let (empty_wire, _) = SessionConfig::default() - .into_wire(Some(SessionId::from("memory-unset"))) - .expect("no duplicate handlers"); - let empty_json = serde_json::to_value(&empty_wire).unwrap(); - assert!(empty_json.get("memory").is_none()); - } - - #[test] - fn resume_session_config_with_memory_serializes() { - let (wire, _runtime) = ResumeSessionConfig::new(SessionId::from("resume-memory-on")) - .with_memory(MemoryConfiguration::enabled()) - .into_wire() - .expect("no duplicate handlers"); - let json = serde_json::to_value(&wire).unwrap(); - assert_eq!(json["memory"], serde_json::json!({ "enabled": true })); - - // Unset memory is omitted on the wire. - let (empty_wire, _) = ResumeSessionConfig::new(SessionId::from("resume-memory-unset")) - .into_wire() - .expect("no duplicate handlers"); - let empty_json = serde_json::to_value(&empty_wire).unwrap(); - assert!(empty_json.get("memory").is_none()); - } - - #[test] - fn feature_flags_serialize_on_create_and_resume() { - let feature_flags = HashMap::from([ - ("BACKGROUND_TASK_NOTIFICATION_PAYLOADS".to_string(), true), - ("DISABLED_TEST_FLAG".to_string(), false), - ]); - let expected = serde_json::json!({ - "BACKGROUND_TASK_NOTIFICATION_PAYLOADS": true, - "DISABLED_TEST_FLAG": false, - }); - - let create_config = SessionConfig::default().with_feature_flags(feature_flags.clone()); - assert_eq!(create_config.feature_flags.as_ref(), Some(&feature_flags)); - let (create_wire, _) = create_config - .into_wire(Some(SessionId::from("feature-flags-create"))) - .expect("no duplicate handlers"); - let create_json = serde_json::to_value(&create_wire).unwrap(); - assert_eq!(create_json["featureFlags"], expected); - - let (resume_wire, _) = ResumeSessionConfig::new(SessionId::from("feature-flags-resume")) - .with_feature_flags(feature_flags) - .into_wire() - .expect("no duplicate handlers"); - let resume_json = serde_json::to_value(&resume_wire).unwrap(); - assert_eq!(resume_json["featureFlags"], expected); - - let (unset_create_wire, _) = SessionConfig::default() - .into_wire(Some(SessionId::from("feature-flags-create-unset"))) - .expect("no duplicate handlers"); - let unset_create_json = serde_json::to_value(&unset_create_wire).unwrap(); - assert!(unset_create_json.get("featureFlags").is_none()); - - let (unset_resume_wire, _) = - ResumeSessionConfig::new(SessionId::from("feature-flags-resume-unset")) - .into_wire() - .expect("no duplicate handlers"); - let unset_resume_json = serde_json::to_value(&unset_resume_wire).unwrap(); - assert!(unset_resume_json.get("featureFlags").is_none()); - } - - fn sample_exp_assignments(context: &str) -> CopilotExpAssignmentResponse { - CopilotExpAssignmentResponse { - features: vec!["copilot_exp_flag".to_string()], - flights: HashMap::from([("copilot_exp_flag".to_string(), "treatment".to_string())]), - configs: vec![ExpConfigEntry { - id: "cfg-1".to_string(), - parameters: HashMap::from([ - ("threshold".to_string(), ExpFlagValue::Integer(5)), - ("enabled".to_string(), ExpFlagValue::Bool(true)), - ]), - }], - assignment_context: context.to_string(), - ..Default::default() - } - } - - #[test] - fn exp_flag_value_round_trips_all_variants() { - let values = serde_json::json!({ - "s": "text", - "i": 7, - "f": 1.5, - "b": true, - "n": null, - }); - let parsed: HashMap = serde_json::from_value(values.clone()).unwrap(); - assert_eq!(parsed["s"], ExpFlagValue::String("text".to_string())); - assert_eq!(parsed["i"], ExpFlagValue::Integer(7)); - assert_eq!(parsed["f"], ExpFlagValue::Float(1.5)); - assert_eq!(parsed["b"], ExpFlagValue::Bool(true)); - assert_eq!(parsed["n"], ExpFlagValue::Null); - assert_eq!(serde_json::to_value(&parsed).unwrap(), values); - } - - #[test] - fn session_config_with_exp_assignments_serializes() { - let assignments = sample_exp_assignments("ctx-123"); - let expected = serde_json::to_value(&assignments).unwrap(); - let (wire, _runtime) = SessionConfig::default() - .with_exp_assignments(assignments) - .into_wire(Some(SessionId::from("exp-on"))) - .expect("no duplicate handlers"); - let json = serde_json::to_value(&wire).unwrap(); - assert_eq!(json["expAssignments"], expected); - assert_eq!(json["expAssignments"]["AssignmentContext"], "ctx-123"); - assert_eq!( - json["expAssignments"]["Flights"]["copilot_exp_flag"], - "treatment" - ); - - // Unset exp assignments are omitted on the wire. - let (empty_wire, _) = SessionConfig::default() - .into_wire(Some(SessionId::from("exp-unset"))) - .expect("no duplicate handlers"); - let empty_json = serde_json::to_value(&empty_wire).unwrap(); - assert!(empty_json.get("expAssignments").is_none()); - } - - #[test] - fn resume_session_config_with_exp_assignments_serializes() { - let assignments = sample_exp_assignments("ctx-456"); - let expected = serde_json::to_value(&assignments).unwrap(); - let (wire, _runtime) = ResumeSessionConfig::new(SessionId::from("resume-exp-on")) - .with_exp_assignments(assignments) - .into_wire() - .expect("no duplicate handlers"); - let json = serde_json::to_value(&wire).unwrap(); - assert_eq!(json["expAssignments"], expected); - - // Unset exp assignments are omitted on the wire. - let (empty_wire, _) = ResumeSessionConfig::new(SessionId::from("resume-exp-unset")) - .into_wire() - .expect("no duplicate handlers"); - let empty_json = serde_json::to_value(&empty_wire).unwrap(); - assert!(empty_json.get("expAssignments").is_none()); - } - - #[test] - fn session_config_clone_preserves_exp_assignments() { - let assignments = sample_exp_assignments("ctx-clone"); - let config = SessionConfig::default().with_exp_assignments(assignments.clone()); - let cloned = config.clone(); - - assert_eq!(cloned.exp_assignments.as_ref(), Some(&assignments)); - - let (wire, _runtime) = cloned - .into_wire(Some(SessionId::from("exp-clone"))) - .expect("no duplicate handlers"); - let json = serde_json::to_value(&wire).unwrap(); - assert_eq!( - json["expAssignments"], - serde_json::to_value(&assignments).unwrap() - ); - } - - #[test] - fn resume_session_config_clone_preserves_exp_assignments() { - let assignments = sample_exp_assignments("ctx-clone-resume"); - let config = ResumeSessionConfig::new(SessionId::from("resume-exp-clone")) - .with_exp_assignments(assignments.clone()); - let cloned = config.clone(); - - assert_eq!(cloned.exp_assignments.as_ref(), Some(&assignments)); - - let (wire, _runtime) = cloned.into_wire().expect("no duplicate handlers"); - let json = serde_json::to_value(&wire).unwrap(); - assert_eq!( - json["expAssignments"], - serde_json::to_value(&assignments).unwrap() - ); - } - - #[test] - #[allow(clippy::field_reassign_with_default)] - fn session_config_into_wire_serializes_bucket_b_fields() { - use std::path::PathBuf; - - use super::{CloudSessionOptions, CloudSessionRepository}; - - let mut cfg = SessionConfig::default(); - cfg.config_directory = Some(PathBuf::from("/tmp/cfg")); - cfg.working_directory = Some(PathBuf::from("/tmp/work")); - cfg.github_token = Some("ghs_secret".to_string()); - cfg.include_sub_agent_streaming_events = Some(false); - cfg.enable_session_telemetry = Some(false); - cfg.reasoning_summary = Some(ReasoningSummary::Concise); - cfg.remote_session = Some(crate::generated::api_types::RemoteSessionMode::Export); - cfg.enable_on_demand_instruction_discovery = Some(false); - cfg.cloud = Some(CloudSessionOptions::with_repository( - CloudSessionRepository::new("github", "copilot-sdk").with_branch("main"), - )); - - let (wire, _runtime) = cfg - .into_wire(Some(SessionId::from("custom-id"))) - .expect("no duplicate handlers"); - let wire_json = serde_json::to_value(&wire).unwrap(); - assert_eq!(wire_json["sessionId"], "custom-id"); - assert_eq!(wire_json["configDir"], "/tmp/cfg"); - assert_eq!(wire_json["workingDirectory"], "/tmp/work"); - assert_eq!(wire_json["gitHubToken"], "ghs_secret"); - assert_eq!(wire_json["includeSubAgentStreamingEvents"], false); - assert_eq!(wire_json["enableSessionTelemetry"], false); - assert_eq!(wire_json["reasoningSummary"], "concise"); - assert_eq!(wire_json["remoteSession"], "export"); - assert_eq!(wire_json["enableOnDemandInstructionDiscovery"], false); - assert_eq!(wire_json["cloud"]["repository"]["owner"], "github"); - assert_eq!(wire_json["cloud"]["repository"]["name"], "copilot-sdk"); - assert_eq!(wire_json["cloud"]["repository"]["branch"], "main"); - - // Unset fields are omitted on the wire. - let (empty_wire, _) = SessionConfig::default() - .into_wire(Some(SessionId::from("empty"))) - .expect("default has no duplicate handlers"); - let empty_json = serde_json::to_value(&empty_wire).unwrap(); - assert!(empty_json.get("gitHubToken").is_none()); - assert!(empty_json.get("enableSessionTelemetry").is_none()); - assert!(empty_json.get("reasoningSummary").is_none()); - assert!(empty_json.get("remoteSession").is_none()); - assert!( - empty_json - .get("enableOnDemandInstructionDiscovery") - .is_none() - ); - assert!(empty_json.get("cloud").is_none()); - } - - #[test] - fn session_config_into_wire_serializes_named_providers_and_models() { - let cfg = SessionConfig::default() - .with_providers(vec![ - NamedProviderConfig::new("my-openai", "https://api.example.com/v1") - .with_provider_type("openai") - .with_wire_api("responses") - .with_model_provider("ollama") - .with_api_key("sk-test"), - ]) - .with_models(vec![ - ProviderModelConfig::new("gpt-x", "my-openai") - .with_wire_model("gpt-x-2025") - .with_max_output_tokens(2048), - ]); - - let (wire, _) = cfg - .into_wire(Some(SessionId::from("sess-providers"))) - .expect("no duplicate handlers"); - let wire_json = serde_json::to_value(&wire).unwrap(); - assert_eq!(wire_json["providers"][0]["name"], "my-openai"); - assert_eq!( - wire_json["providers"][0]["baseUrl"], - "https://api.example.com/v1" - ); - assert_eq!(wire_json["providers"][0]["type"], "openai"); - assert_eq!(wire_json["providers"][0]["wireApi"], "responses"); - assert_eq!(wire_json["providers"][0]["modelProvider"], "ollama"); - assert_eq!(wire_json["providers"][0]["apiKey"], "sk-test"); - assert_eq!(wire_json["models"][0]["id"], "gpt-x"); - assert_eq!(wire_json["models"][0]["provider"], "my-openai"); - assert_eq!(wire_json["models"][0]["wireModel"], "gpt-x-2025"); - assert_eq!(wire_json["models"][0]["maxOutputTokens"], 2048); - - let (empty_wire, _) = SessionConfig::default() - .into_wire(Some(SessionId::from("empty"))) - .expect("default has no duplicate handlers"); - let empty_json = serde_json::to_value(&empty_wire).unwrap(); - assert!(empty_json.get("providers").is_none()); - assert!(empty_json.get("models").is_none()); - } - - #[test] - fn resume_config_into_wire_serializes_named_providers_and_models() { - let cfg = ResumeSessionConfig::new(SessionId::from("sess-resume")) - .with_providers(vec![ - NamedProviderConfig::new("my-azure", "https://example.openai.azure.com") - .with_provider_type("azure") - .with_azure(AzureProviderOptions { - api_version: Some("2024-10-21".to_string()), - }), - ]) - .with_models(vec![ - ProviderModelConfig::new("deploy-1", "my-azure").with_model_id("gpt-4o"), - ]); - - let (wire, _) = cfg.into_wire().expect("no duplicate handlers"); - let wire_json = serde_json::to_value(&wire).unwrap(); - assert_eq!(wire_json["providers"][0]["name"], "my-azure"); - assert_eq!(wire_json["providers"][0]["type"], "azure"); - assert_eq!( - wire_json["providers"][0]["azure"]["apiVersion"], - "2024-10-21" - ); - assert_eq!(wire_json["models"][0]["id"], "deploy-1"); - assert_eq!(wire_json["models"][0]["provider"], "my-azure"); - assert_eq!(wire_json["models"][0]["modelId"], "gpt-4o"); - - let (empty_wire, _) = ResumeSessionConfig::new(SessionId::from("empty")) - .into_wire() - .expect("default has no duplicate handlers"); - let empty_json = serde_json::to_value(&empty_wire).unwrap(); - assert!(empty_json.get("providers").is_none()); - assert!(empty_json.get("models").is_none()); - } - - #[test] - fn session_config_into_wire_serializes_plugin_directories_and_large_output() { - use std::path::PathBuf; - - let cfg = SessionConfig { - plugin_directories: Some(vec![PathBuf::from("/tmp/plugins")]), - disabled_mcp_servers: Some(vec![ - "local-files".to_string(), - "remote-github".to_string(), - ]), - large_output: Some( - LargeToolOutputConfig::new() - .with_enabled(true) - .with_max_size_bytes(1024) - .with_output_directory(PathBuf::from("/tmp/large-output")), - ), - ..Default::default() - }; - - let (wire, _) = cfg - .into_wire(Some(SessionId::from("sess-1"))) - .expect("no duplicate handlers"); - let wire_json = serde_json::to_value(&wire).unwrap(); - assert_eq!(wire_json["pluginDirectories"][0], "/tmp/plugins"); - assert_eq!( - wire_json["disabledMcpServers"], - serde_json::json!(["local-files", "remote-github"]) - ); - assert_eq!(wire_json["largeOutput"]["enabled"], true); - assert_eq!(wire_json["largeOutput"]["maxSizeBytes"], 1024); - assert_eq!(wire_json["largeOutput"]["outputDir"], "/tmp/large-output"); - - let (empty_wire, _) = SessionConfig::default() - .into_wire(Some(SessionId::from("empty"))) - .expect("default has no duplicate handlers"); - let empty_json = serde_json::to_value(&empty_wire).unwrap(); - assert!(empty_json.get("pluginDirectories").is_none()); - assert!(empty_json.get("disabledMcpServers").is_none()); - assert!(empty_json.get("largeOutput").is_none()); - } - - #[test] - fn resume_session_config_into_wire_serializes_bucket_b_fields() { - use std::path::PathBuf; - - let mut cfg = ResumeSessionConfig::new(SessionId::from("sess-1")); - cfg.working_directory = Some(PathBuf::from("/tmp/work")); - cfg.config_directory = Some(PathBuf::from("/tmp/cfg")); - cfg.github_token = Some("ghs_secret".to_string()); - cfg.include_sub_agent_streaming_events = Some(true); - cfg.enable_session_telemetry = Some(false); - cfg.reasoning_summary = Some(ReasoningSummary::Detailed); - cfg.remote_session = Some(crate::generated::api_types::RemoteSessionMode::On); - cfg.enable_on_demand_instruction_discovery = Some(false); - - let (wire, _) = cfg.into_wire().expect("no duplicate handlers"); - let wire_json = serde_json::to_value(&wire).unwrap(); - assert_eq!(wire_json["sessionId"], "sess-1"); - assert_eq!(wire_json["workingDirectory"], "/tmp/work"); - assert_eq!(wire_json["configDir"], "/tmp/cfg"); - assert_eq!(wire_json["gitHubToken"], "ghs_secret"); - assert_eq!(wire_json["includeSubAgentStreamingEvents"], true); - assert_eq!(wire_json["enableSessionTelemetry"], false); - assert_eq!(wire_json["reasoningSummary"], "detailed"); - assert_eq!(wire_json["remoteSession"], "on"); - assert_eq!(wire_json["enableOnDemandInstructionDiscovery"], false); - - // Unset remote_session is omitted on the wire. - let (empty_wire, _) = ResumeSessionConfig::new(SessionId::from("sess-2")) - .into_wire() - .expect("default resume has no duplicate handlers"); - let empty_json = serde_json::to_value(&empty_wire).unwrap(); - assert!(empty_json.get("reasoningSummary").is_none()); - assert!(empty_json.get("remoteSession").is_none()); - assert!( - empty_json - .get("enableOnDemandInstructionDiscovery") - .is_none() - ); - } - - #[test] - fn resume_session_config_into_wire_serializes_plugin_directories_and_large_output() { - use std::path::PathBuf; - - let mut cfg = ResumeSessionConfig::new(SessionId::from("sess-1")); - cfg.plugin_directories = Some(vec![PathBuf::from("/tmp/plugins-r")]); - cfg.disabled_mcp_servers = Some(vec!["local-files-r".to_string()]); - cfg.large_output = Some( - LargeToolOutputConfig::new() - .with_enabled(false) - .with_max_size_bytes(2048) - .with_output_directory(PathBuf::from("/tmp/large-output-r")), - ); - - let (wire, _) = cfg.into_wire().expect("no duplicate handlers"); - let wire_json = serde_json::to_value(&wire).unwrap(); - assert_eq!(wire_json["pluginDirectories"][0], "/tmp/plugins-r"); - assert_eq!( - wire_json["disabledMcpServers"], - serde_json::json!(["local-files-r"]) - ); - assert_eq!(wire_json["largeOutput"]["enabled"], false); - assert_eq!(wire_json["largeOutput"]["maxSizeBytes"], 2048); - assert_eq!(wire_json["largeOutput"]["outputDir"], "/tmp/large-output-r"); - - let (empty_wire, _) = ResumeSessionConfig::new(SessionId::from("sess-2")) - .into_wire() - .expect("default resume has no duplicate handlers"); - let empty_json = serde_json::to_value(&empty_wire).unwrap(); - assert!(empty_json.get("pluginDirectories").is_none()); - assert!(empty_json.get("disabledMcpServers").is_none()); - assert!(empty_json.get("largeOutput").is_none()); - } - - #[test] - fn auth_client_id_metadata_url_reaches_create_and_resume_wire_payloads() { - let url = "https://example.com/oauth/client-metadata.json"; - - let (create_wire, _) = SessionConfig::default() - .with_auth_client_id_metadata_url(url) - .into_wire(None) - .expect("default create has no duplicate handlers"); - let create_json = serde_json::to_value(&create_wire).unwrap(); - assert_eq!(create_json["authClientIdMetadataUrl"], url); - - let (resume_wire, _) = ResumeSessionConfig::new(SessionId::from("sess-1")) - .with_auth_client_id_metadata_url(url) - .into_wire() - .expect("default resume has no duplicate handlers"); - let resume_json = serde_json::to_value(&resume_wire).unwrap(); - assert_eq!(resume_json["authClientIdMetadataUrl"], url); - - let (empty_create_wire, _) = SessionConfig::default() - .into_wire(None) - .expect("default create has no duplicate handlers"); - let empty_create_json = serde_json::to_value(&empty_create_wire).unwrap(); - assert!(empty_create_json.get("authClientIdMetadataUrl").is_none()); - - let (empty_resume_wire, _) = ResumeSessionConfig::new(SessionId::from("sess-2")) - .into_wire() - .expect("default resume has no duplicate handlers"); - let empty_resume_json = serde_json::to_value(&empty_resume_wire).unwrap(); - assert!(empty_resume_json.get("authClientIdMetadataUrl").is_none()); - } - - #[test] - fn session_config_clones_disabled_mcp_servers() { - let create = SessionConfig::default().with_disabled_mcp_servers(["local-files"]); - let mut create_clone = create.clone(); - create_clone - .disabled_mcp_servers - .as_mut() - .expect("configured disabled MCP servers") - .push("remote-github".to_string()); - assert_eq!( - create.disabled_mcp_servers.as_deref(), - Some(&["local-files".to_string()][..]) - ); - - let resume = ResumeSessionConfig::new(SessionId::from("sess-1")) - .with_disabled_mcp_servers(["local-files"]); - let mut resume_clone = resume.clone(); - resume_clone - .disabled_mcp_servers - .as_mut() - .expect("configured disabled MCP servers") - .push("remote-github".to_string()); - assert_eq!( - resume.disabled_mcp_servers.as_deref(), - Some(&["local-files".to_string()][..]) - ); - } - - #[test] - fn session_config_builder_composes() { - use indexmap::IndexMap; - - let cfg = SessionConfig::default() - .with_session_id(SessionId::from("sess-1")) - .with_model("claude-sonnet-4") - .with_client_name("test-app") - .with_reasoning_effort("medium") - .with_reasoning_summary(ReasoningSummary::Concise) - .with_context_tier("long_context") - .with_streaming(true) - .with_tools([Tool::new("greet")]) - .with_available_tools(["bash", "view"]) - .with_excluded_tools(["dangerous"]) - .with_mcp_servers(IndexMap::new()) - .with_mcp_oauth_token_storage("persistent") - .with_enable_config_discovery(true) - .with_enable_on_demand_instruction_discovery(true) - .with_skill_directories([PathBuf::from("/tmp/skills")]) - .with_disabled_skills(["broken-skill"]) - .with_disabled_mcp_servers(["local-files"]) - .with_agent("researcher") - .with_config_directory(PathBuf::from("/tmp/config")) - .with_working_directory(PathBuf::from("/tmp/work")) - .with_additional_directories([PathBuf::from("/tmp/shared")]) - .with_github_token("ghp_test") - .with_capi(CapiSessionOptions::new().with_enable_web_socket_responses(false)) - .with_enable_session_telemetry(false) - .with_include_sub_agent_streaming_events(false) - .with_extension_info(ExtensionInfo::new("github-app", "counter")); - - assert_eq!(cfg.session_id.as_ref().map(|s| s.as_str()), Some("sess-1")); - assert_eq!(cfg.model.as_deref(), Some("claude-sonnet-4")); - assert_eq!(cfg.client_name.as_deref(), Some("test-app")); - assert_eq!(cfg.reasoning_effort.as_deref(), Some("medium")); - assert_eq!(cfg.reasoning_summary, Some(ReasoningSummary::Concise)); - assert_eq!(cfg.context_tier.as_deref(), Some("long_context")); - assert_eq!(cfg.streaming, Some(true)); - assert_eq!(cfg.tools.as_ref().map(|t| t.len()), Some(1)); - assert_eq!( - cfg.available_tools.as_deref(), - Some(&["bash".to_string(), "view".to_string()][..]) - ); - assert_eq!( - cfg.excluded_tools.as_deref(), - Some(&["dangerous".to_string()][..]) - ); - assert!(cfg.mcp_servers.is_some()); - assert_eq!(cfg.mcp_oauth_token_storage.as_deref(), Some("persistent")); - assert_eq!(cfg.enable_config_discovery, Some(true)); - assert_eq!(cfg.enable_on_demand_instruction_discovery, Some(true)); - assert_eq!( - cfg.skill_directories.as_deref(), - Some(&[PathBuf::from("/tmp/skills")][..]) - ); - assert_eq!( - cfg.disabled_skills.as_deref(), - Some(&["broken-skill".to_string()][..]) - ); - assert_eq!( - cfg.disabled_mcp_servers.as_deref(), - Some(&["local-files".to_string()][..]) - ); - assert_eq!(cfg.agent.as_deref(), Some("researcher")); - assert_eq!(cfg.config_directory, Some(PathBuf::from("/tmp/config"))); - assert_eq!(cfg.working_directory, Some(PathBuf::from("/tmp/work"))); - assert_eq!( - cfg.additional_directories.as_deref(), - Some(&[PathBuf::from("/tmp/shared")][..]) - ); - assert_eq!(cfg.github_token.as_deref(), Some("ghp_test")); - assert_eq!( - cfg.capi, - Some(CapiSessionOptions::new().with_enable_web_socket_responses(false)) - ); - assert_eq!(cfg.enable_session_telemetry, Some(false)); - assert_eq!(cfg.include_sub_agent_streaming_events, Some(false)); - assert_eq!( - cfg.extension_info, - Some(ExtensionInfo::new("github-app", "counter")) - ); - } - - #[test] - fn resume_session_config_builder_composes() { - use indexmap::IndexMap; - - let cfg = ResumeSessionConfig::new(SessionId::from("sess-2")) - .with_client_name("test-app") - .with_reasoning_summary(ReasoningSummary::None) - .with_context_tier("default") - .with_streaming(true) - .with_tools([Tool::new("greet")]) - .with_available_tools(["bash", "view"]) - .with_excluded_tools(["dangerous"]) - .with_mcp_servers(IndexMap::new()) - .with_mcp_oauth_token_storage("persistent") - .with_enable_config_discovery(true) - .with_enable_on_demand_instruction_discovery(false) - .with_skill_directories([PathBuf::from("/tmp/skills")]) - .with_disabled_skills(["broken-skill"]) - .with_disabled_mcp_servers(["local-files"]) - .with_agent("researcher") - .with_config_directory(PathBuf::from("/tmp/config")) - .with_working_directory(PathBuf::from("/tmp/work")) - .with_additional_directories([PathBuf::from("/tmp/shared")]) - .with_github_token("ghp_test") - .with_capi(CapiSessionOptions::new().with_enable_web_socket_responses(false)) - .with_enable_session_telemetry(false) - .with_include_sub_agent_streaming_events(true) - .with_suppress_resume_event(true) - .with_continue_pending_work(true) - .with_extension_info(ExtensionInfo::new("github-app", "counter")); - - assert_eq!(cfg.session_id.as_str(), "sess-2"); - assert_eq!(cfg.client_name.as_deref(), Some("test-app")); - assert_eq!(cfg.reasoning_summary, Some(ReasoningSummary::None)); - assert_eq!(cfg.context_tier.as_deref(), Some("default")); - assert_eq!(cfg.streaming, Some(true)); - assert_eq!(cfg.tools.as_ref().map(|t| t.len()), Some(1)); - assert_eq!( - cfg.available_tools.as_deref(), - Some(&["bash".to_string(), "view".to_string()][..]) - ); - assert_eq!( - cfg.excluded_tools.as_deref(), - Some(&["dangerous".to_string()][..]) - ); - assert!(cfg.mcp_servers.is_some()); - assert_eq!(cfg.mcp_oauth_token_storage.as_deref(), Some("persistent")); - assert_eq!(cfg.enable_config_discovery, Some(true)); - assert_eq!(cfg.enable_on_demand_instruction_discovery, Some(false)); - assert_eq!( - cfg.skill_directories.as_deref(), - Some(&[PathBuf::from("/tmp/skills")][..]) - ); - assert_eq!( - cfg.disabled_skills.as_deref(), - Some(&["broken-skill".to_string()][..]) - ); - assert_eq!( - cfg.disabled_mcp_servers.as_deref(), - Some(&["local-files".to_string()][..]) - ); - assert_eq!(cfg.agent.as_deref(), Some("researcher")); - assert_eq!(cfg.config_directory, Some(PathBuf::from("/tmp/config"))); - assert_eq!(cfg.working_directory, Some(PathBuf::from("/tmp/work"))); - assert_eq!( - cfg.additional_directories.as_deref(), - Some(&[PathBuf::from("/tmp/shared")][..]) - ); - assert_eq!(cfg.github_token.as_deref(), Some("ghp_test")); - assert_eq!( - cfg.capi, - Some(CapiSessionOptions::new().with_enable_web_socket_responses(false)) - ); - assert_eq!(cfg.enable_session_telemetry, Some(false)); - assert_eq!(cfg.include_sub_agent_streaming_events, Some(true)); - assert_eq!(cfg.suppress_resume_event, Some(true)); - assert_eq!(cfg.continue_pending_work, Some(true)); - assert_eq!( - cfg.extension_info, - Some(ExtensionInfo::new("github-app", "counter")) - ); - } - - /// `continue_pending_work` must serialize to wire as `continuePendingWork` - /// — the runtime keys off this exact field name to opt into the - /// pending-work-handoff pattern. - #[test] - fn resume_session_config_serializes_continue_pending_work_to_camel_case() { - let cfg = - ResumeSessionConfig::new(SessionId::from("sess-1")).with_continue_pending_work(true); - let (wire, _) = cfg.into_wire().expect("no duplicate handlers"); - let json = serde_json::to_value(&wire).unwrap(); - assert_eq!(json["continuePendingWork"], true); - - // Unset case — skip_serializing_if must omit the field. - let (wire, _) = ResumeSessionConfig::new(SessionId::from("sess-2")) - .into_wire() - .expect("no duplicate handlers"); - let json = serde_json::to_value(&wire).unwrap(); - assert!(json.get("continuePendingWork").is_none()); - } - - #[test] - fn resume_policy_and_recovery_report_round_trip() { - let config = ResumeSessionConfig::new(SessionId::from("sess-1")) - .with_allow_transcript_recovery(false); - let (wire, _) = config.into_wire().unwrap(); - let value = serde_json::to_value(&wire).unwrap(); - assert_eq!(value["allowTranscriptRecovery"], false); - - let (wire, _) = ResumeSessionConfig::new(SessionId::from("sess-2")) - .into_wire() - .unwrap(); - assert!( - serde_json::to_value(&wire) - .unwrap() - .get("allowTranscriptRecovery") - .is_none() - ); - - let result: crate::types::ResumeSessionResult = serde_json::from_value(serde_json::json!({ - "sessionId": "sess-1", - "transcriptRecovery": { - "plannedBackupPath": "events.jsonl.backup", - "invalidLineNumbers": [2], - "sessionStartMoved": false - } - })) - .unwrap(); - let recovery = result.transcript_recovery.unwrap(); - assert_eq!(recovery.invalid_line_numbers, vec![2]); - assert_eq!(recovery.planned_backup_path, "events.jsonl.backup"); - } - - #[test] - fn session_configs_serialize_additional_directories() { - let create = SessionConfig::default().with_additional_directories([ - PathBuf::from("/tmp/shared"), - PathBuf::from("/tmp/generated"), - ]); - let (create_wire, _) = create.into_wire(None).expect("no duplicate handlers"); - let create_json = serde_json::to_value(&create_wire).unwrap(); - assert_eq!( - create_json["additionalDirectories"], - serde_json::json!(["/tmp/shared", "/tmp/generated"]) - ); - - let resume = ResumeSessionConfig::new(SessionId::from("sess-1")) - .with_additional_directories([PathBuf::from("/tmp/resumed")]); - let (resume_wire, _) = resume.into_wire().expect("no duplicate handlers"); - let resume_json = serde_json::to_value(&resume_wire).unwrap(); - assert_eq!( - resume_json["additionalDirectories"], - serde_json::json!(["/tmp/resumed"]) - ); - } - - /// The Rust field is `suppress_resume_event`, but the wire field stays - /// `disableResume` to preserve compatibility with the runtime and other - /// SDKs. - #[test] - fn resume_session_config_serializes_suppress_resume_event_to_disable_resume_on_wire() { - let cfg = - ResumeSessionConfig::new(SessionId::from("sess-1")).with_suppress_resume_event(true); - let (wire, _) = cfg.into_wire().expect("no duplicate handlers"); - let json = serde_json::to_value(&wire).unwrap(); - assert_eq!(json["disableResume"], true); - assert!(json.get("suppressResumeEvent").is_none()); - } - - /// `instruction_directories` must serialize to wire as - /// `instructionDirectories` on `SessionConfig`. - #[test] - fn session_config_serializes_instruction_directories_to_camel_case() { - let cfg = - SessionConfig::default().with_instruction_directories([PathBuf::from("/tmp/instr")]); - let (wire, _) = cfg - .into_wire(Some(SessionId::from("instr-on"))) - .expect("no duplicate handlers"); - let json = serde_json::to_value(&wire).unwrap(); - assert_eq!( - json["instructionDirectories"], - serde_json::json!(["/tmp/instr"]) - ); - - // Unset case — skip_serializing_if must omit the field. - let (wire, _) = SessionConfig::default() - .into_wire(Some(SessionId::from("instr-off"))) - .expect("no duplicate handlers"); - let json = serde_json::to_value(&wire).unwrap(); - assert!(json.get("instructionDirectories").is_none()); - } - - /// Same check on the resume path. Forwarded to the CLI on - /// `session.resume`. - #[test] - fn resume_session_config_serializes_instruction_directories_to_camel_case() { - let cfg = ResumeSessionConfig::new(SessionId::from("sess-1")) - .with_instruction_directories([PathBuf::from("/tmp/instr")]); - let (wire, _) = cfg.into_wire().expect("no duplicate handlers"); - let json = serde_json::to_value(&wire).unwrap(); - assert_eq!( - json["instructionDirectories"], - serde_json::json!(["/tmp/instr"]) - ); - - let (wire, _) = ResumeSessionConfig::new(SessionId::from("sess-2")) - .into_wire() - .expect("no duplicate handlers"); - let json = serde_json::to_value(&wire).unwrap(); - assert!(json.get("instructionDirectories").is_none()); - } - - #[test] - fn custom_agent_config_builder_composes() { - use indexmap::IndexMap; - - let cfg = CustomAgentConfig::new("researcher", "You are a research assistant.") - .with_display_name("Research Assistant") - .with_description("Investigates technical questions.") - .with_tools(["bash", "view"]) - .with_mcp_servers(IndexMap::new()) - .with_infer(true) - .with_skills(["rust-coding-skill"]); - - assert_eq!(cfg.name, "researcher"); - assert_eq!(cfg.prompt, "You are a research assistant."); - assert_eq!(cfg.display_name.as_deref(), Some("Research Assistant")); - assert_eq!( - cfg.description.as_deref(), - Some("Investigates technical questions.") - ); - assert_eq!( - cfg.tools.as_deref(), - Some(&["bash".to_string(), "view".to_string()][..]) - ); - assert!(cfg.mcp_servers.is_some()); - assert_eq!(cfg.infer, Some(true)); - assert_eq!( - cfg.skills.as_deref(), - Some(&["rust-coding-skill".to_string()][..]) - ); - } - - #[test] - fn mcp_servers_serialize_in_insertion_order() { - use indexmap::IndexMap; - - // Regression: `mcp_servers` was a `HashMap`, so the server keys (and - // thus the `session.create` payload) serialized in a per-process - // random order; `IndexMap` pins them to insertion order. The long - // sequence makes a `HashMap` regression reproduce this exact order by - // chance only 1/N!, avoiding a flaky false pass. - let order = [ - "zebra", "quartz", "delta", "ivy", "mango", "bravo", "xenon", "amber", "falcon", - "ceres", "nova", "kelp", "otter", "yodel", "plum", "garnet", - ]; - let mut servers = IndexMap::new(); - for name in order { - servers.insert( - name.to_string(), - McpServerConfig::Stdio(McpStdioServerConfig { - command: "run".to_string(), - ..Default::default() - }), - ); - } - - let (wire, _runtime) = SessionConfig::default() - .with_mcp_servers(servers) - .into_wire(None) - .expect("into_wire should succeed"); - let json = serde_json::to_string(&wire).expect("serialize wire"); - - let positions: Vec = order - .iter() - .map(|name| { - json.find(&format!("\"{name}\"")) - .unwrap_or_else(|| panic!("server {name} missing from wire JSON")) - }) - .collect(); - let mut ascending = positions.clone(); - ascending.sort_unstable(); - assert_eq!( - positions, ascending, - "mcp server keys must serialize in insertion order: {json}" - ); - } - - #[test] - fn infinite_session_config_builder_composes() { - let cfg = InfiniteSessionConfig::new() - .with_enabled(true) - .with_background_compaction_threshold(0.75) - .with_buffer_exhaustion_threshold(0.92); - - assert_eq!(cfg.enabled, Some(true)); - assert_eq!(cfg.background_compaction_threshold, Some(0.75)); - assert_eq!(cfg.buffer_exhaustion_threshold, Some(0.92)); - } - - #[test] - fn provider_config_builder_composes() { - use std::collections::HashMap; - - let mut headers = HashMap::new(); - headers.insert("X-Custom".to_string(), "value".to_string()); - - let cfg = ProviderConfig::new("https://api.example.com") - .with_provider_type("openai") - .with_wire_api("completions") - .with_transport("websockets") - .with_model_provider("lm_studio") - .with_api_key("sk-test") - .with_bearer_token("bearer-test") - .with_headers(headers) - .with_model_id("gpt-4") - .with_wire_model("azure-gpt-4-deployment") - .with_max_prompt_tokens(8192) - .with_max_output_tokens(2048); - - assert_eq!(cfg.base_url, "https://api.example.com"); - assert_eq!(cfg.provider_type.as_deref(), Some("openai")); - assert_eq!(cfg.wire_api.as_deref(), Some("completions")); - assert_eq!(cfg.transport.as_deref(), Some("websockets")); - assert_eq!(cfg.model_provider.as_deref(), Some("lm_studio")); - assert_eq!(cfg.api_key.as_deref(), Some("sk-test")); - assert_eq!(cfg.bearer_token.as_deref(), Some("bearer-test")); - assert_eq!( - cfg.headers - .as_ref() - .and_then(|h| h.get("X-Custom")) - .map(String::as_str), - Some("value"), - ); - assert_eq!(cfg.model_id.as_deref(), Some("gpt-4")); - assert_eq!(cfg.wire_model.as_deref(), Some("azure-gpt-4-deployment")); - assert_eq!(cfg.max_prompt_tokens, Some(8192)); - assert_eq!(cfg.max_output_tokens, Some(2048)); - - // Wire-shape: camelCase, skip_serializing_if when unset. - let wire = serde_json::to_value(&cfg).unwrap(); - assert_eq!(wire["modelId"], "gpt-4"); - assert_eq!(wire["wireModel"], "azure-gpt-4-deployment"); - assert_eq!(wire["modelProvider"], "lm_studio"); - assert_eq!(wire["maxPromptTokens"], 8192); - assert_eq!(wire["maxOutputTokens"], 2048); - - let unset = ProviderConfig::new("https://api.example.com"); - let wire_unset = serde_json::to_value(&unset).unwrap(); - assert!(wire_unset.get("modelId").is_none()); - assert!(wire_unset.get("wireModel").is_none()); - assert!(wire_unset.get("modelProvider").is_none()); - assert!(wire_unset.get("maxPromptTokens").is_none()); - assert!(wire_unset.get("maxOutputTokens").is_none()); - } - - #[test] - fn capi_session_options_builder_composes_and_serializes() { - let cfg = CapiSessionOptions::new().with_enable_web_socket_responses(false); - - assert_eq!(cfg.enable_web_socket_responses, Some(false)); - - let wire = serde_json::to_value(&cfg).unwrap(); - assert_eq!( - wire, - serde_json::json!({ "enableWebSocketResponses": false }) - ); - - let unset = CapiSessionOptions::new(); - let wire_unset = serde_json::to_value(&unset).unwrap(); - assert!(wire_unset.get("enableWebSocketResponses").is_none()); - assert!(wire_unset.get("autoTier").is_none()); - assert_eq!(wire_unset, json!({})); - } - - #[test] - fn capi_auto_tier_canonical_values_round_trip_and_forward() { - for (tier, value) in [ - (AutoTier::Efficiency, "efficiency"), - (AutoTier::Balance, "balance"), - (AutoTier::Intelligence, "intelligence"), - (AutoTier::Fast, "fast"), - ] { - let exported: crate::AutoTier = tier.clone(); - let capi = CapiSessionOptions::new().with_auto_tier(exported); - assert_eq!(capi.auto_tier, Some(tier)); - assert_eq!( - serde_json::to_value(&capi).unwrap(), - json!({"autoTier": value}) - ); - assert_eq!( - serde_json::from_value::(json!({"autoTier": value})).unwrap(), - capi - ); - - let capi = capi.with_enable_web_socket_responses(false); - let expected = json!({"autoTier": value, "enableWebSocketResponses": false}); - let (create, _) = SessionConfig::default() - .with_model("auto") - .with_capi(capi.clone()) - .into_wire(Some(SessionId::from("capi-create"))) - .unwrap(); - assert_eq!(serde_json::to_value(create).unwrap()["capi"], expected); - - let (resume, _) = ResumeSessionConfig::new(SessionId::from("capi-resume")) - .with_capi(capi) - .into_wire() - .unwrap(); - assert_eq!(serde_json::to_value(resume).unwrap()["capi"], expected); - } - } - - #[test] - fn capi_auto_tier_accepts_unknown_values_for_forward_compatibility() { - for value in ["balanced", "Balance", "unknown"] { - assert_eq!( - serde_json::from_value::(json!(value)).unwrap(), - AutoTier::Unknown - ); - } - let capi: CapiSessionOptions = serde_json::from_value(json!({})).unwrap(); - assert_eq!(capi.auto_tier, None); - } - - #[test] - fn session_config_with_capi_serializes() { - let (wire, _) = SessionConfig::default() - .with_capi(CapiSessionOptions::new().with_enable_web_socket_responses(false)) - .into_wire(Some(SessionId::from("capi-create"))) - .expect("no duplicate handlers"); - let json = serde_json::to_value(&wire).unwrap(); - assert_eq!( - json["capi"], - serde_json::json!({ "enableWebSocketResponses": false }) - ); - - let (empty_wire, _) = SessionConfig::default() - .into_wire(Some(SessionId::from("capi-create-unset"))) - .expect("no duplicate handlers"); - let empty_json = serde_json::to_value(&empty_wire).unwrap(); - assert!(empty_json.get("capi").is_none()); - } - - #[test] - fn resume_session_config_with_capi_serializes() { - let (wire, _) = ResumeSessionConfig::new(SessionId::from("capi-resume")) - .with_capi(CapiSessionOptions::new().with_enable_web_socket_responses(false)) - .into_wire() - .expect("no duplicate handlers"); - let json = serde_json::to_value(&wire).unwrap(); - assert_eq!( - json["capi"], - serde_json::json!({ "enableWebSocketResponses": false }) - ); - - let (empty_wire, _) = ResumeSessionConfig::new(SessionId::from("capi-resume-unset")) - .into_wire() - .expect("no duplicate handlers"); - let empty_json = serde_json::to_value(&empty_wire).unwrap(); - assert!(empty_json.get("capi").is_none()); - } - - #[test] - fn system_message_config_builder_composes() { - use std::collections::HashMap; - - let cfg = SystemMessageConfig::new() - .with_mode("replace") - .with_content("Custom system message.") - .with_sections(HashMap::new()); - - assert_eq!(cfg.mode.as_deref(), Some("replace")); - assert_eq!(cfg.content.as_deref(), Some("Custom system message.")); - assert!(cfg.sections.is_some()); - } - - #[test] - fn delivery_mode_serializes_to_kebab_case_strings() { - assert_eq!( - serde_json::to_string(&DeliveryMode::Enqueue).unwrap(), - "\"enqueue\"" - ); - assert_eq!( - serde_json::to_string(&DeliveryMode::Immediate).unwrap(), - "\"immediate\"" - ); - let parsed: DeliveryMode = serde_json::from_str("\"immediate\"").unwrap(); - assert_eq!(parsed, DeliveryMode::Immediate); - } - - #[test] - fn agent_mode_serializes_to_kebab_case_strings() { - assert_eq!( - serde_json::to_string(&AgentMode::Interactive).unwrap(), - "\"interactive\"" - ); - assert_eq!(serde_json::to_string(&AgentMode::Plan).unwrap(), "\"plan\""); - assert_eq!( - serde_json::to_string(&AgentMode::Autopilot).unwrap(), - "\"autopilot\"" - ); - assert_eq!( - serde_json::to_string(&AgentMode::Shell).unwrap(), - "\"shell\"" - ); - let parsed: AgentMode = serde_json::from_str("\"plan\"").unwrap(); - assert_eq!(parsed, AgentMode::Plan); - } - - #[test] - fn connection_state_distinguishes_variants() { - // ConnectionState is now an internal type; verify we can construct - // and compare the variants used by the lifecycle code paths. - assert_ne!(ConnectionState::Connected, ConnectionState::Disconnected); - } - - /// `agentId` is the sub-agent attribution field added in copilot-sdk - /// commit f8cf846 ("Derive session event envelopes from schema"). - /// Every other SDK (Node, Python, Go, .NET) carries it on the event - /// envelope; Rust must too or sub-agent events lose attribution at - /// the deserialization boundary. Cross-SDK parity test. - #[test] - fn session_event_round_trips_agent_id_on_envelope() { - let wire = json!({ - "id": "evt-1", - "timestamp": "2026-04-30T12:00:00Z", - "parentId": null, - "agentId": "sub-agent-42", - "type": "assistant.message", - "data": { "message": "hi" } - }); - - let event: SessionEvent = serde_json::from_value(wire.clone()).unwrap(); - assert_eq!(event.agent_id.as_deref(), Some("sub-agent-42")); - - // Round-trip preserves the field on the wire. - let roundtripped = serde_json::to_value(&event).unwrap(); - assert_eq!(roundtripped["agentId"], "sub-agent-42"); - - // Absent agentId remains absent (skip_serializing_if). - let main_agent_event: SessionEvent = serde_json::from_value(json!({ - "id": "evt-2", - "timestamp": "2026-04-30T12:00:01Z", - "parentId": null, - "type": "session.idle", - "data": {} - })) - .unwrap(); - assert!(main_agent_event.agent_id.is_none()); - let roundtripped = serde_json::to_value(&main_agent_event).unwrap(); - assert!(roundtripped.get("agentId").is_none()); - } - - /// Same parity for the typed event envelope produced by the codegen. - #[test] - fn typed_session_event_round_trips_agent_id_on_envelope() { - let wire = json!({ - "id": "evt-1", - "timestamp": "2026-04-30T12:00:00Z", - "parentId": null, - "agentId": "sub-agent-42", - "type": "session.idle", - "data": {} - }); - - let event: TypedSessionEvent = serde_json::from_value(wire).unwrap(); - assert_eq!(event.agent_id.as_deref(), Some("sub-agent-42")); - - let roundtripped = serde_json::to_value(&event).unwrap(); - assert_eq!(roundtripped["agentId"], "sub-agent-42"); - } - - #[test] - fn connection_state_variants_compile() { - // Defensive smoke test: all variants must be constructable from - // within the crate. (The enum was demoted from pub to pub(crate) - // in Phase D; this test guards against accidental removal.) - let _ = ConnectionState::Disconnected; - let _ = ConnectionState::Connecting; - let _ = ConnectionState::Connected; - let _ = ConnectionState::Error; - } - - #[test] - fn deserializes_runtime_attachment_variants() { - let attachments: Vec = serde_json::from_value(json!([ - { - "type": "file", - "path": "/tmp/file.rs", - "displayName": "file.rs", - "lineRange": { "start": 7, "end": 12 } - }, - { - "type": "directory", - "path": "/tmp/project", - "displayName": "project" - }, - { - "type": "selection", - "filePath": "/tmp/lib.rs", - "displayName": "lib.rs", - "text": "fn main() {}", - "selection": { - "start": { "line": 1, "character": 2 }, - "end": { "line": 3, "character": 4 } - } - }, - { - "type": "blob", - "data": "Zm9v", - "mimeType": "image/png", - "displayName": "image.png" - }, - { - "type": "github_reference", - "number": 42, - "title": "Fix rendering", - "referenceType": "issue", - "state": "open", - "url": "https://github.com/example/repo/issues/42" - }, - { - "type": "extension_context", - "capturedAt": "2026-09-18T11:00:00Z", - "extensionId": "example:extension", - "title": "Unbound context" - } - ])) - .expect("attachments should deserialize"); - - assert_eq!(attachments.len(), 6); - assert!(matches!( - &attachments[0], - Attachment::File { - path, - display_name, - line_range: Some(AttachmentLineRange { start: 7, end: 12 }), - } if path == &PathBuf::from("/tmp/file.rs") && display_name.as_deref() == Some("file.rs") - )); - assert!(matches!( - &attachments[1], - Attachment::Directory { path, display_name } - if path == &PathBuf::from("/tmp/project") && display_name.as_deref() == Some("project") - )); - assert!(matches!( - &attachments[2], - Attachment::Selection { - file_path, - display_name, - selection: - AttachmentSelectionRange { - start: AttachmentSelectionPosition { line: 1, character: 2 }, - end: AttachmentSelectionPosition { line: 3, character: 4 }, - }, - .. - } if file_path == &PathBuf::from("/tmp/lib.rs") && display_name.as_deref() == Some("lib.rs") - )); - assert!(matches!( - &attachments[3], - Attachment::Blob { - data, - mime_type, - display_name, - } if data == "Zm9v" && mime_type == "image/png" && display_name.as_deref() == Some("image.png") - )); - assert!(matches!( - &attachments[4], - Attachment::GitHubReference { - number: 42, - title, - reference_type: GitHubReferenceType::Issue, - state, - url, - } if title == "Fix rendering" - && state == "open" - && url == "https://github.com/example/repo/issues/42" - )); - assert!(matches!( - &attachments[5], - Attachment::ExtensionContext { - captured_at, - extension_id, - canvas_id: None, - instance_id: None, - title, - payload: None, - } if captured_at == "2026-09-18T11:00:00Z" - && extension_id == "example:extension" - && title == "Unbound context" - )); - assert_eq!( - serde_json::to_value(&attachments[5]).expect("serialize extension context"), - json!({ - "type": "extension_context", - "capturedAt": "2026-09-18T11:00:00Z", - "extensionId": "example:extension", - "title": "Unbound context" - }) - ); - } - - #[test] - fn ensures_display_names_for_variants_that_support_them() { - let mut attachments = vec![ - Attachment::File { - path: PathBuf::from("/tmp/file.rs"), - display_name: None, - line_range: None, - }, - Attachment::Selection { - file_path: PathBuf::from("/tmp/src/lib.rs"), - display_name: None, - text: "fn main() {}".to_string(), - selection: AttachmentSelectionRange { - start: AttachmentSelectionPosition { - line: 0, - character: 0, - }, - end: AttachmentSelectionPosition { - line: 0, - character: 10, - }, - }, - }, - Attachment::Blob { - data: "Zm9v".to_string(), - mime_type: "image/png".to_string(), - display_name: None, - }, - Attachment::GitHubReference { - number: 7, - title: "Track regressions".to_string(), - reference_type: GitHubReferenceType::Issue, - state: "open".to_string(), - url: "https://example.com/issues/7".to_string(), - }, - ]; - - ensure_attachment_display_names(&mut attachments); - - assert_eq!(attachments[0].display_name(), Some("file.rs")); - assert_eq!(attachments[1].display_name(), Some("lib.rs")); - assert_eq!(attachments[2].display_name(), Some("attachment")); - assert_eq!(attachments[3].display_name(), None); - assert_eq!( - attachments[3].label(), - Some("Track regressions".to_string()) - ); - } - - #[test] - fn github_anchored_attachment_variants_round_trip() { - let cases = vec![ - ( - "github_commit", - json!({ - "type": "github_commit", - "message": "Fix the thing", - "oid": "abc123", - "repo": { "id": 1, "name": "repo", "owner": "octocat" }, - "url": "https://github.com/octocat/repo/commit/abc123" - }), - ), - ( - "github_release", - json!({ - "type": "github_release", - "name": "v1.2.3", - "repo": { "name": "repo", "owner": "octocat" }, - "tagName": "v1.2.3", - "url": "https://github.com/octocat/repo/releases/tag/v1.2.3" - }), - ), - ( - "github_actions_job", - json!({ - "type": "github_actions_job", - "conclusion": "failure", - "jobId": 99, - "jobName": "build", - "repo": { "name": "repo", "owner": "octocat" }, - "url": "https://github.com/octocat/repo/actions/runs/1/job/99", - "workflowName": "CI" - }), - ), - ( - "github_repository", - json!({ - "type": "github_repository", - "description": "An example repository", - "ref": "main", - "repo": { "name": "repo", "owner": "octocat" }, - "url": "https://github.com/octocat/repo" - }), - ), - ( - "github_file_diff", - json!({ - "type": "github_file_diff", - "base": { - "path": "src/lib.rs", - "ref": "main", - "repo": { "name": "repo", "owner": "octocat" } - }, - "head": { - "path": "src/lib.rs", - "ref": "feature", - "repo": { "name": "repo", "owner": "octocat" } - }, - "url": "https://github.com/octocat/repo/compare/main...feature" - }), - ), - ( - "github_tree_comparison", - json!({ - "type": "github_tree_comparison", - "base": { - "repo": { "name": "repo", "owner": "octocat" }, - "revision": "main" - }, - "head": { - "repo": { "name": "repo", "owner": "octocat" }, - "revision": "feature" - }, - "url": "https://github.com/octocat/repo/compare/main...feature" - }), - ), - ( - "github_url", - json!({ - "type": "github_url", - "url": "https://github.com/octocat/repo/wiki" - }), - ), - ( - "github_file", - json!({ - "type": "github_file", - "path": "src/main.rs", - "ref": "main", - "repo": { "name": "repo", "owner": "octocat" }, - "url": "https://github.com/octocat/repo/blob/main/src/main.rs" - }), - ), - ( - "github_snippet", - json!({ - "type": "github_snippet", - "lineRange": { "start": 10, "end": 20 }, - "path": "src/main.rs", - "ref": "main", - "repo": { "name": "repo", "owner": "octocat" }, - "url": "https://github.com/octocat/repo/blob/main/src/main.rs#L10-L20" - }), - ), - ]; - - for (expected_type, input) in cases { - let attachment: Attachment = serde_json::from_value(input.clone()) - .unwrap_or_else(|err| panic!("{expected_type} should deserialize: {err}")); - - // Serialize to a string first: parsing into `serde_json::Value` would - // silently dedupe a duplicate `type` key, hiding the exact regression - // this test guards against (e.g. a wrapped generated struct emitting its - // own `type` alongside the enum tag). - let serialized_string = serde_json::to_string(&attachment) - .unwrap_or_else(|err| panic!("{expected_type} should serialize: {err}")); - - // Exactly one `type` key, carrying the expected discriminator. - assert_eq!( - serialized_string.matches("\"type\":").count(), - 1, - "{expected_type} must serialize a single `type` key" - ); - - let serialized: serde_json::Value = serde_json::from_str(&serialized_string) - .unwrap_or_else(|err| panic!("{expected_type} should reparse: {err}")); - assert_eq!( - serialized.get("type").and_then(|value| value.as_str()), - Some(expected_type), - "{expected_type} must serialize the correct discriminator" - ); - - // Round-trips without dropping fields. - assert_eq!( - serialized, input, - "{expected_type} should round-trip without data loss" - ); - let reparsed: Attachment = serde_json::from_value(serialized) - .unwrap_or_else(|err| panic!("{expected_type} should re-deserialize: {err}")); - assert_eq!( - reparsed, attachment, - "{expected_type} should re-deserialize to the same value" - ); - } - } -} +mod tests; #[cfg(test)] -mod permission_builder_tests { - use std::sync::Arc; - - use crate::handler::{ApproveAllHandler, PermissionHandler, PermissionResult}; - use crate::permission; - use crate::types::{ - PermissionDecision, PermissionRequestData, RequestId, ResumeSessionConfig, SessionConfig, - SessionId, - }; - - fn data() -> PermissionRequestData { - PermissionRequestData { - extra: serde_json::json!({"tool": "shell"}), - ..Default::default() - } - } - - /// Apply the same policy-resolution logic that `Client::create_session` - /// uses, so tests exercise the effective handler. - fn resolve_create(mut cfg: SessionConfig) -> Option> { - permission::resolve_handler(cfg.permission_handler.take(), cfg.permission_policy.take()) - } - - fn resolve_resume(mut cfg: ResumeSessionConfig) -> Option> { - permission::resolve_handler(cfg.permission_handler.take(), cfg.permission_policy.take()) - } - - async fn dispatch(handler: &Arc) -> PermissionResult { - handler - .handle(SessionId::from("s1"), RequestId::new("1"), data()) - .await - } - - #[tokio::test] - async fn approve_all_with_handler_present_approves() { - let cfg = SessionConfig::default() - .with_permission_handler(Arc::new(ApproveAllHandler)) - .approve_all_permissions(); - let h = resolve_create(cfg).expect("policy + handler yields handler"); - assert!(matches!( - dispatch(&h).await, - PermissionResult::Decision { - decision: PermissionDecision::ApproveOnce(_), - .. - } - )); - } - - #[tokio::test] - async fn approve_all_standalone_produces_handler() { - let cfg = SessionConfig::default().approve_all_permissions(); - let h = resolve_create(cfg).expect("policy alone yields handler"); - assert!(matches!( - dispatch(&h).await, - PermissionResult::Decision { - decision: PermissionDecision::ApproveOnce(_), - .. - } - )); - } - - /// Phase I: order between with_permission_handler and the policy - /// builder must not matter. - #[tokio::test] - async fn approve_all_is_order_independent() { - let a = SessionConfig::default() - .with_permission_handler(Arc::new(ApproveAllHandler)) - .approve_all_permissions(); - let b = SessionConfig::default() - .approve_all_permissions() - .with_permission_handler(Arc::new(ApproveAllHandler)); - let ha = resolve_create(a).unwrap(); - let hb = resolve_create(b).unwrap(); - assert!(matches!( - dispatch(&ha).await, - PermissionResult::Decision { - decision: PermissionDecision::ApproveOnce(_), - .. - } - )); - assert!(matches!( - dispatch(&hb).await, - PermissionResult::Decision { - decision: PermissionDecision::ApproveOnce(_), - .. - } - )); - } - - #[tokio::test] - async fn deny_all_is_order_independent() { - let a = SessionConfig::default() - .with_permission_handler(Arc::new(ApproveAllHandler)) - .deny_all_permissions(); - let b = SessionConfig::default() - .deny_all_permissions() - .with_permission_handler(Arc::new(ApproveAllHandler)); - let ha = resolve_create(a).unwrap(); - let hb = resolve_create(b).unwrap(); - assert!(matches!( - dispatch(&ha).await, - PermissionResult::Decision { - decision: PermissionDecision::Reject(_), - .. - } - )); - assert!(matches!( - dispatch(&hb).await, - PermissionResult::Decision { - decision: PermissionDecision::Reject(_), - .. - } - )); - } - - #[tokio::test] - async fn approve_permissions_if_consults_predicate() { - let cfg = SessionConfig::default().approve_permissions_if(|d| { - d.extra.get("tool").and_then(|v| v.as_str()) != Some("shell") - }); - let h = resolve_create(cfg).unwrap(); - assert!(matches!( - dispatch(&h).await, - PermissionResult::Decision { - decision: PermissionDecision::Reject(_), - .. - } - )); - } - - #[tokio::test] - async fn approve_permissions_if_is_order_independent() { - let predicate = |d: &PermissionRequestData| { - d.extra.get("tool").and_then(|v| v.as_str()) != Some("shell") - }; - let a = SessionConfig::default() - .with_permission_handler(Arc::new(ApproveAllHandler)) - .approve_permissions_if(predicate); - let b = SessionConfig::default() - .approve_permissions_if(predicate) - .with_permission_handler(Arc::new(ApproveAllHandler)); - let ha = resolve_create(a).unwrap(); - let hb = resolve_create(b).unwrap(); - assert!(matches!( - dispatch(&ha).await, - PermissionResult::Decision { - decision: PermissionDecision::Reject(_), - .. - } - )); - assert!(matches!( - dispatch(&hb).await, - PermissionResult::Decision { - decision: PermissionDecision::Reject(_), - .. - } - )); - } - - #[tokio::test] - async fn resume_session_config_approve_all_works() { - let cfg = ResumeSessionConfig::new(SessionId::from("s1")) - .with_permission_handler(Arc::new(ApproveAllHandler)) - .approve_all_permissions(); - let h = resolve_resume(cfg).unwrap(); - assert!(matches!( - dispatch(&h).await, - PermissionResult::Decision { - decision: PermissionDecision::ApproveOnce(_), - .. - } - )); - } - - #[tokio::test] - async fn resume_session_config_approve_all_is_order_independent() { - let a = ResumeSessionConfig::new(SessionId::from("s1")) - .with_permission_handler(Arc::new(ApproveAllHandler)) - .approve_all_permissions(); - let b = ResumeSessionConfig::new(SessionId::from("s1")) - .approve_all_permissions() - .with_permission_handler(Arc::new(ApproveAllHandler)); - let ha = resolve_resume(a).unwrap(); - let hb = resolve_resume(b).unwrap(); - assert!(matches!( - dispatch(&ha).await, - PermissionResult::Decision { - decision: PermissionDecision::ApproveOnce(_), - .. - } - )); - assert!(matches!( - dispatch(&hb).await, - PermissionResult::Decision { - decision: PermissionDecision::ApproveOnce(_), - .. - } - )); - } - - #[test] - fn session_config_enable_experimental_mode_serializes_when_set() { - let cfg = SessionConfig::default().with_enable_experimental_mode(false); - assert_eq!(cfg.enable_experimental_mode, Some(false)); - - let (wire, _runtime) = cfg - .into_wire(Some(SessionId::from("experimental-mode"))) - .expect("enable_experimental_mode config has no duplicate handlers"); - assert_eq!(wire.is_experimental_mode, Some(false)); - - let json = serde_json::to_value(&wire).unwrap(); - assert_eq!(json["isExperimentalMode"], serde_json::Value::Bool(false)); - } - - #[test] - fn session_config_enable_experimental_mode_omitted_when_none() { - let cfg = SessionConfig::default(); - assert_eq!(cfg.enable_experimental_mode, None); - - let (wire, _runtime) = cfg - .into_wire(Some(SessionId::from("no-experimental-mode"))) - .expect("default config has no duplicate handlers"); - assert_eq!(wire.is_experimental_mode, None); - - let json = serde_json::to_value(&wire).unwrap(); - assert!(json.get("isExperimentalMode").is_none()); - } - - #[test] - fn resume_session_config_enable_experimental_mode_serializes_when_set() { - let cfg = ResumeSessionConfig::new(SessionId::from("resume-experimental-mode")) - .with_enable_experimental_mode(false); - assert_eq!(cfg.enable_experimental_mode, Some(false)); - - let (wire, _runtime) = cfg - .into_wire() - .expect("resume enable_experimental_mode config has no duplicate handlers"); - assert_eq!(wire.is_experimental_mode, Some(false)); - - let json = serde_json::to_value(&wire).unwrap(); - assert_eq!(json["isExperimentalMode"], serde_json::Value::Bool(false)); - } - - #[test] - fn resume_session_config_enable_experimental_mode_omitted_when_none() { - let cfg = ResumeSessionConfig::new(SessionId::from("resume-no-experimental-mode")); - assert_eq!(cfg.enable_experimental_mode, None); - - let (wire, _runtime) = cfg - .into_wire() - .expect("default resume config has no duplicate handlers"); - assert_eq!(wire.is_experimental_mode, None); - - let json = serde_json::to_value(&wire).unwrap(); - assert!(json.get("isExperimentalMode").is_none()); - } -} +mod permission_builder_tests; #[cfg(test)] -mod is_terminal_tests { - use super::Tool; - - #[test] - fn is_terminal_serializes_as_camel_case_when_set() { - let tool = Tool { - name: "clear_context".to_owned(), - is_terminal: true, - ..Default::default() - }; - let value = serde_json::to_value(&tool).expect("tool serializes"); - assert_eq!( - value.get("isTerminal"), - Some(&serde_json::Value::Bool(true)) - ); - } - - #[test] - fn is_terminal_is_omitted_when_false() { - let tool = Tool { - name: "plain".to_owned(), - ..Default::default() - }; - let value = serde_json::to_value(&tool).expect("tool serializes"); - assert!(value.get("isTerminal").is_none()); - } - - /// `Tool` has a hand-written `Debug` impl, so a new field is only reported - /// if it is added there by hand. Guard against that drift. - #[test] - fn is_terminal_appears_in_debug_output() { - let terminal = Tool { - name: "clear_context".to_owned(), - is_terminal: true, - ..Default::default() - }; - assert!(format!("{terminal:?}").contains("is_terminal: true")); - - let plain = Tool { - name: "plain".to_owned(), - ..Default::default() - }; - assert!(format!("{plain:?}").contains("is_terminal: false")); - } -} +mod is_terminal_tests; #[cfg(test)] mod refresh_custom_instructions_tests; diff --git a/rust/src/types/is_terminal_tests.rs b/rust/src/types/is_terminal_tests.rs new file mode 100644 index 0000000000..8e0090ab36 --- /dev/null +++ b/rust/src/types/is_terminal_tests.rs @@ -0,0 +1,49 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +#![cfg(test)] + +use super::Tool; + +#[test] +fn is_terminal_serializes_as_camel_case_when_set() { + let tool = Tool { + name: "clear_context".to_owned(), + is_terminal: true, + ..Default::default() + }; + let value = serde_json::to_value(&tool).expect("tool serializes"); + assert_eq!( + value.get("isTerminal"), + Some(&serde_json::Value::Bool(true)) + ); +} + +#[test] +fn is_terminal_is_omitted_when_false() { + let tool = Tool { + name: "plain".to_owned(), + ..Default::default() + }; + let value = serde_json::to_value(&tool).expect("tool serializes"); + assert!(value.get("isTerminal").is_none()); +} + +/// `Tool` has a hand-written `Debug` impl, so a new field is only reported +/// if it is added there by hand. Guard against that drift. +#[test] +fn is_terminal_appears_in_debug_output() { + let terminal = Tool { + name: "clear_context".to_owned(), + is_terminal: true, + ..Default::default() + }; + assert!(format!("{terminal:?}").contains("is_terminal: true")); + + let plain = Tool { + name: "plain".to_owned(), + ..Default::default() + }; + assert!(format!("{plain:?}").contains("is_terminal: false")); +} diff --git a/rust/src/types/permission_builder_tests.rs b/rust/src/types/permission_builder_tests.rs new file mode 100644 index 0000000000..c549f9d4d8 --- /dev/null +++ b/rust/src/types/permission_builder_tests.rs @@ -0,0 +1,259 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +#![cfg(test)] + +use std::sync::Arc; + +use crate::handler::{ApproveAllHandler, PermissionHandler, PermissionResult}; +use crate::permission; +use crate::types::{ + PermissionDecision, PermissionRequestData, RequestId, ResumeSessionConfig, SessionConfig, + SessionId, +}; + +fn data() -> PermissionRequestData { + PermissionRequestData { + extra: serde_json::json!({"tool": "shell"}), + ..Default::default() + } +} + +/// Apply the same policy-resolution logic that `Client::create_session` +/// uses, so tests exercise the effective handler. +fn resolve_create(mut cfg: SessionConfig) -> Option> { + permission::resolve_handler(cfg.permission_handler.take(), cfg.permission_policy.take()) +} + +fn resolve_resume(mut cfg: ResumeSessionConfig) -> Option> { + permission::resolve_handler(cfg.permission_handler.take(), cfg.permission_policy.take()) +} + +async fn dispatch(handler: &Arc) -> PermissionResult { + handler + .handle(SessionId::from("s1"), RequestId::new("1"), data()) + .await +} + +#[tokio::test] +async fn approve_all_with_handler_present_approves() { + let cfg = SessionConfig::default() + .with_permission_handler(Arc::new(ApproveAllHandler)) + .approve_all_permissions(); + let h = resolve_create(cfg).expect("policy + handler yields handler"); + assert!(matches!( + dispatch(&h).await, + PermissionResult::Decision { + decision: PermissionDecision::ApproveOnce(_), + .. + } + )); +} + +#[tokio::test] +async fn approve_all_standalone_produces_handler() { + let cfg = SessionConfig::default().approve_all_permissions(); + let h = resolve_create(cfg).expect("policy alone yields handler"); + assert!(matches!( + dispatch(&h).await, + PermissionResult::Decision { + decision: PermissionDecision::ApproveOnce(_), + .. + } + )); +} + +/// Phase I: order between with_permission_handler and the policy +/// builder must not matter. +#[tokio::test] +async fn approve_all_is_order_independent() { + let a = SessionConfig::default() + .with_permission_handler(Arc::new(ApproveAllHandler)) + .approve_all_permissions(); + let b = SessionConfig::default() + .approve_all_permissions() + .with_permission_handler(Arc::new(ApproveAllHandler)); + let ha = resolve_create(a).unwrap(); + let hb = resolve_create(b).unwrap(); + assert!(matches!( + dispatch(&ha).await, + PermissionResult::Decision { + decision: PermissionDecision::ApproveOnce(_), + .. + } + )); + assert!(matches!( + dispatch(&hb).await, + PermissionResult::Decision { + decision: PermissionDecision::ApproveOnce(_), + .. + } + )); +} + +#[tokio::test] +async fn deny_all_is_order_independent() { + let a = SessionConfig::default() + .with_permission_handler(Arc::new(ApproveAllHandler)) + .deny_all_permissions(); + let b = SessionConfig::default() + .deny_all_permissions() + .with_permission_handler(Arc::new(ApproveAllHandler)); + let ha = resolve_create(a).unwrap(); + let hb = resolve_create(b).unwrap(); + assert!(matches!( + dispatch(&ha).await, + PermissionResult::Decision { + decision: PermissionDecision::Reject(_), + .. + } + )); + assert!(matches!( + dispatch(&hb).await, + PermissionResult::Decision { + decision: PermissionDecision::Reject(_), + .. + } + )); +} + +#[tokio::test] +async fn approve_permissions_if_consults_predicate() { + let cfg = SessionConfig::default() + .approve_permissions_if(|d| d.extra.get("tool").and_then(|v| v.as_str()) != Some("shell")); + let h = resolve_create(cfg).unwrap(); + assert!(matches!( + dispatch(&h).await, + PermissionResult::Decision { + decision: PermissionDecision::Reject(_), + .. + } + )); +} + +#[tokio::test] +async fn approve_permissions_if_is_order_independent() { + let predicate = + |d: &PermissionRequestData| d.extra.get("tool").and_then(|v| v.as_str()) != Some("shell"); + let a = SessionConfig::default() + .with_permission_handler(Arc::new(ApproveAllHandler)) + .approve_permissions_if(predicate); + let b = SessionConfig::default() + .approve_permissions_if(predicate) + .with_permission_handler(Arc::new(ApproveAllHandler)); + let ha = resolve_create(a).unwrap(); + let hb = resolve_create(b).unwrap(); + assert!(matches!( + dispatch(&ha).await, + PermissionResult::Decision { + decision: PermissionDecision::Reject(_), + .. + } + )); + assert!(matches!( + dispatch(&hb).await, + PermissionResult::Decision { + decision: PermissionDecision::Reject(_), + .. + } + )); +} + +#[tokio::test] +async fn resume_session_config_approve_all_works() { + let cfg = ResumeSessionConfig::new(SessionId::from("s1")) + .with_permission_handler(Arc::new(ApproveAllHandler)) + .approve_all_permissions(); + let h = resolve_resume(cfg).unwrap(); + assert!(matches!( + dispatch(&h).await, + PermissionResult::Decision { + decision: PermissionDecision::ApproveOnce(_), + .. + } + )); +} + +#[tokio::test] +async fn resume_session_config_approve_all_is_order_independent() { + let a = ResumeSessionConfig::new(SessionId::from("s1")) + .with_permission_handler(Arc::new(ApproveAllHandler)) + .approve_all_permissions(); + let b = ResumeSessionConfig::new(SessionId::from("s1")) + .approve_all_permissions() + .with_permission_handler(Arc::new(ApproveAllHandler)); + let ha = resolve_resume(a).unwrap(); + let hb = resolve_resume(b).unwrap(); + assert!(matches!( + dispatch(&ha).await, + PermissionResult::Decision { + decision: PermissionDecision::ApproveOnce(_), + .. + } + )); + assert!(matches!( + dispatch(&hb).await, + PermissionResult::Decision { + decision: PermissionDecision::ApproveOnce(_), + .. + } + )); +} + +#[test] +fn session_config_enable_experimental_mode_serializes_when_set() { + let cfg = SessionConfig::default().with_enable_experimental_mode(false); + assert_eq!(cfg.enable_experimental_mode, Some(false)); + + let (wire, _runtime) = cfg + .into_wire(Some(SessionId::from("experimental-mode"))) + .expect("enable_experimental_mode config has no duplicate handlers"); + assert_eq!(wire.is_experimental_mode, Some(false)); + + let json = serde_json::to_value(&wire).unwrap(); + assert_eq!(json["isExperimentalMode"], serde_json::Value::Bool(false)); +} + +#[test] +fn session_config_enable_experimental_mode_omitted_when_none() { + let cfg = SessionConfig::default(); + assert_eq!(cfg.enable_experimental_mode, None); + + let (wire, _runtime) = cfg + .into_wire(Some(SessionId::from("no-experimental-mode"))) + .expect("default config has no duplicate handlers"); + assert_eq!(wire.is_experimental_mode, None); + + let json = serde_json::to_value(&wire).unwrap(); + assert!(json.get("isExperimentalMode").is_none()); +} + +#[test] +fn resume_session_config_enable_experimental_mode_serializes_when_set() { + let cfg = ResumeSessionConfig::new(SessionId::from("resume-experimental-mode")) + .with_enable_experimental_mode(false); + assert_eq!(cfg.enable_experimental_mode, Some(false)); + + let (wire, _runtime) = cfg + .into_wire() + .expect("resume enable_experimental_mode config has no duplicate handlers"); + assert_eq!(wire.is_experimental_mode, Some(false)); + + let json = serde_json::to_value(&wire).unwrap(); + assert_eq!(json["isExperimentalMode"], serde_json::Value::Bool(false)); +} + +#[test] +fn resume_session_config_enable_experimental_mode_omitted_when_none() { + let cfg = ResumeSessionConfig::new(SessionId::from("resume-no-experimental-mode")); + assert_eq!(cfg.enable_experimental_mode, None); + + let (wire, _runtime) = cfg + .into_wire() + .expect("default resume config has no duplicate handlers"); + assert_eq!(wire.is_experimental_mode, None); + + let json = serde_json::to_value(&wire).unwrap(); + assert!(json.get("isExperimentalMode").is_none()); +} diff --git a/rust/src/types/tests.rs b/rust/src/types/tests.rs new file mode 100644 index 0000000000..a61c083a4f --- /dev/null +++ b/rust/src/types/tests.rs @@ -0,0 +1,1927 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +#![cfg(test)] + +use std::collections::HashMap; +use std::path::PathBuf; + +use serde_json::json; + +use super::{ + AgentMode, Attachment, AttachmentLineRange, AttachmentSelectionPosition, + AttachmentSelectionRange, AutoTier, AzureProviderOptions, CapiSessionOptions, ConnectionState, + CopilotExpAssignmentResponse, CustomAgentConfig, DeliveryMode, ExpConfigEntry, ExpFlagValue, + ExtensionInfo, GitHubMcpToolConfig, GitHubReferenceType, InfiniteSessionConfig, + LargeToolOutputConfig, McpServerConfig, McpStdioServerConfig, MemoryConfiguration, + NamedProviderConfig, PermissionResponseCapability, ProviderConfig, ProviderModelConfig, + ReasoningSummary, ResumeSessionConfig, SessionConfig, SessionEvent, SessionId, + SystemMessageConfig, Tool, ToolBinaryResult, ToolResult, ToolResultExpanded, + ToolResultResponse, ensure_attachment_display_names, +}; +use crate::generated::session_events::TypedSessionEvent; + +#[test] +fn permission_response_capability_is_publicly_exported() { + assert_eq!( + serde_json::to_value(PermissionResponseCapability::Interactive).unwrap(), + json!("interactive") + ); +} + +#[test] +fn tool_builder_composes() { + let tool = Tool::new("greet") + .with_description("Say hello") + .with_namespaced_name("hello/greet") + .with_instructions("Pass the user's name") + .with_parameters(json!({ + "type": "object", + "properties": { "name": { "type": "string" } }, + "required": ["name"] + })) + .with_overrides_built_in_tool(true) + .with_skip_permission(true); + assert_eq!(tool.name, "greet"); + assert_eq!(tool.description, "Say hello"); + assert_eq!(tool.namespaced_name.as_deref(), Some("hello/greet")); + assert_eq!(tool.instructions.as_deref(), Some("Pass the user's name")); + assert_eq!(tool.parameters.get("type").unwrap(), &json!("object")); + assert!(tool.overrides_built_in_tool); + assert!(tool.skip_permission); +} + +#[test] +fn tool_defer_serialization() { + let tool = Tool::new("lookup").with_defer(super::DeferMode::Auto); + assert_eq!(tool.defer, Some(super::DeferMode::Auto)); + let value = serde_json::to_value(&tool).unwrap(); + assert_eq!(value.get("defer").unwrap(), &json!("auto")); + + let plain = Tool::new("plain"); + let value = serde_json::to_value(&plain).unwrap(); + assert!(value.get("defer").is_none()); +} + +#[test] +fn tool_metadata_serialization() { + use indexmap::IndexMap; + + let mut metadata = IndexMap::new(); + metadata.insert( + "github.com/copilot:safeForTelemetry".to_string(), + json!({ "name": true, "inputsNames": false }), + ); + let tool = Tool::new("lookup").with_metadata(metadata); + let value = serde_json::to_value(&tool).unwrap(); + assert_eq!( + value + .get("metadata") + .unwrap() + .get("github.com/copilot:safeForTelemetry") + .unwrap(), + &json!({ "name": true, "inputsNames": false }) + ); + + // Empty metadata is omitted on the wire. + let plain = Tool::new("plain"); + let value = serde_json::to_value(&plain).unwrap(); + assert!(value.get("metadata").is_none()); +} + +#[test] +fn custom_agent_config_builder_with_model() { + let agent = CustomAgentConfig::new("my-agent", "You are helpful.") + .with_model("claude-haiku-4.5") + .with_display_name("My Agent"); + assert_eq!(agent.name, "my-agent"); + assert_eq!(agent.model.as_deref(), Some("claude-haiku-4.5")); + assert_eq!(agent.display_name.as_deref(), Some("My Agent")); +} + +#[test] +fn custom_agent_config_serializes_model() { + let agent = CustomAgentConfig::new("model-agent", "prompt").with_model("claude-haiku-4.5"); + let wire = serde_json::to_value(&agent).unwrap(); + assert_eq!(wire["model"], "claude-haiku-4.5"); + assert_eq!(wire["name"], "model-agent"); +} + +#[test] +fn custom_agent_config_omits_model_when_none() { + let agent = CustomAgentConfig::new("no-model-agent", "prompt"); + let wire = serde_json::to_value(&agent).unwrap(); + assert!(wire.get("model").is_none()); +} + +#[test] +fn custom_agent_config_builder_with_reasoning_effort() { + let agent = CustomAgentConfig::new("reasoning-agent", "prompt").with_reasoning_effort("high"); + assert_eq!(agent.reasoning_effort.as_deref(), Some("high")); +} + +#[test] +fn custom_agent_config_serializes_reasoning_effort() { + let agent = CustomAgentConfig::new("reasoning-agent", "prompt").with_reasoning_effort("high"); + let wire = serde_json::to_value(&agent).unwrap(); + assert_eq!(wire["reasoningEffort"], "high"); +} + +#[test] +fn custom_agent_config_omits_reasoning_effort_when_none() { + let agent = CustomAgentConfig::new("default-agent", "prompt"); + let wire = serde_json::to_value(&agent).unwrap(); + assert!(wire.get("reasoningEffort").is_none()); +} + +#[test] +#[should_panic(expected = "tool parameter schema must be a JSON object")] +fn tool_with_parameters_panics_on_non_object_value() { + let _ = Tool::new("noop").with_parameters(json!(null)); +} + +#[test] +fn tool_result_expanded_serializes_binary_results_for_llm() { + let response = ToolResultResponse { + result: ToolResult::Expanded(ToolResultExpanded { + text_result_for_llm: "rendered chart".to_string(), + result_type: "success".to_string(), + binary_results_for_llm: Some(vec![ToolBinaryResult { + data: "aW1n".to_string(), + mime_type: "image/png".to_string(), + r#type: "image".to_string(), + description: Some("chart preview".to_string()), + }]), + session_log: None, + error: None, + tool_telemetry: None, + tool_references: None, + }), + }; + + let wire = serde_json::to_value(&response).unwrap(); + + assert_eq!( + wire, + json!({ + "result": { + "textResultForLlm": "rendered chart", + "resultType": "success", + "binaryResultsForLlm": [ + { + "data": "aW1n", + "mimeType": "image/png", + "type": "image", + "description": "chart preview" + } + ] + } + }) + ); +} + +#[test] +fn tool_result_expanded_omits_binary_results_for_llm_when_none() { + let response = ToolResultResponse { + result: ToolResult::Expanded(ToolResultExpanded { + text_result_for_llm: "ok".to_string(), + result_type: "success".to_string(), + binary_results_for_llm: None, + session_log: None, + error: None, + tool_telemetry: None, + tool_references: None, + }), + }; + + let wire = serde_json::to_value(&response).unwrap(); + + assert_eq!(wire["result"]["textResultForLlm"], "ok"); + assert!(wire["result"].get("binaryResultsForLlm").is_none()); +} + +#[test] +fn tool_result_expanded_serializes_tool_references() { + let response = ToolResultResponse { + result: ToolResult::Expanded( + ToolResultExpanded::new("found 2 tools", "success") + .with_tool_references(["get_weather", "check_status"]), + ), + }; + + let wire = serde_json::to_value(&response).unwrap(); + + assert_eq!( + wire, + json!({ + "result": { + "textResultForLlm": "found 2 tools", + "resultType": "success", + "toolReferences": ["get_weather", "check_status"] + } + }) + ); +} + +#[test] +fn tool_result_expanded_omits_tool_references_when_none() { + let response = ToolResultResponse { + result: ToolResult::Expanded(ToolResultExpanded::new("ok", "success")), + }; + + let wire = serde_json::to_value(&response).unwrap(); + + assert_eq!(wire["result"]["textResultForLlm"], "ok"); + assert!(wire["result"].get("toolReferences").is_none()); +} + +#[test] +fn tool_result_expanded_with_tool_references_accepts_owned_strings() { + // The builder is generic over `Into`, so an owned `Vec` + // must compile and populate the field just like a `&str` array. + let names: Vec = vec!["alpha".to_string(), "beta".to_string()]; + let expanded = ToolResultExpanded::new("ok", "success").with_tool_references(names); + + assert_eq!( + expanded.tool_references.as_deref(), + Some(["alpha".to_string(), "beta".to_string()].as_slice()) + ); +} + +#[test] +fn tool_result_expanded_deserializes_tool_references() { + let wire = json!({ + "textResultForLlm": "found tools", + "resultType": "success", + "toolReferences": ["alpha", "beta"] + }); + + let expanded: ToolResultExpanded = serde_json::from_value(wire).unwrap(); + + assert_eq!( + expanded.tool_references.as_deref(), + Some(["alpha".to_string(), "beta".to_string()].as_slice()) + ); +} + +#[test] +fn session_config_default_wire_flags_off_without_handlers() { + let cfg = SessionConfig::default(); + assert_eq!(cfg.mcp_oauth_token_storage, None); + assert_eq!(cfg.allowed_models, None); + // Wire flags are derived from handler presence at create_session + // time, not stored on the config. With no handlers installed, every + // request_* flag should serialize as false. + let (wire, _runtime) = cfg + .into_wire(Some(SessionId::from("default-flags"))) + .expect("default config has no duplicate handlers"); + assert!(!wire.request_user_input); + assert!(!wire.request_permission); + assert!(!wire.request_elicitation); + assert!(!wire.request_exit_plan_mode); + assert!(!wire.request_auto_mode_switch); + assert!(!wire.hooks); + assert!(!wire.request_mcp_apps); + let json = serde_json::to_value(&wire).unwrap(); + assert!(json.get("askUserVariant").is_none()); + assert!(json.get("allowedModels").is_none()); +} + +#[test] +fn resume_session_config_new_wire_flags_off_without_handlers() { + let cfg = ResumeSessionConfig::new(SessionId::from("resume-flags")); + assert_eq!(cfg.mcp_oauth_token_storage, None); + assert_eq!(cfg.allowed_models, None); + let (wire, _runtime) = cfg + .into_wire() + .expect("default resume config has no duplicate handlers"); + assert!(!wire.request_user_input); + assert!(!wire.request_permission); + assert!(!wire.request_elicitation); + assert!(!wire.request_exit_plan_mode); + assert!(!wire.request_auto_mode_switch); + assert!(!wire.hooks); + assert!(!wire.request_mcp_apps); + let json = serde_json::to_value(&wire).unwrap(); + assert!(json.get("askUserVariant").is_none()); + assert!(json.get("allowedModels").is_none()); +} + +#[test] +fn session_configs_build_debug_and_serialize_allowed_models() { + let create = SessionConfig::default().with_allowed_models(["gpt-5.4", "claude-sonnet-4"]); + assert_eq!( + create.allowed_models.as_deref(), + Some(&["gpt-5.4".to_string(), "claude-sonnet-4".to_string()][..]) + ); + assert!(format!("{create:?}").contains("allowed_models")); + + let (create_wire, _) = create + .into_wire(Some(SessionId::from("create-allowed-models"))) + .expect("allowed model config has no duplicate handlers"); + let create_json = serde_json::to_value(&create_wire).unwrap(); + assert_eq!( + create_json["allowedModels"], + json!(["gpt-5.4", "claude-sonnet-4"]) + ); + + let resume = ResumeSessionConfig::new(SessionId::from("resume-allowed-models")) + .with_allowed_models(vec!["gpt-5.4".to_string(), "gpt-5-mini".to_string()]); + assert_eq!( + resume.allowed_models.as_deref(), + Some(&["gpt-5.4".to_string(), "gpt-5-mini".to_string()][..]) + ); + assert!(format!("{resume:?}").contains("allowed_models")); + + let (resume_wire, _) = resume + .into_wire() + .expect("resume allowed model config has no duplicate handlers"); + let resume_json = serde_json::to_value(&resume_wire).unwrap(); + assert_eq!( + resume_json["allowedModels"], + json!(["gpt-5.4", "gpt-5-mini"]) + ); +} + +#[test] +fn custom_agents_local_only_serializes_on_create_and_resume() { + let (create_wire, _) = SessionConfig::default() + .with_custom_agents_local_only(false) + .into_wire(Some(SessionId::from("create-locality"))) + .expect("create config has no duplicate handlers"); + let create_json = serde_json::to_value(&create_wire).unwrap(); + assert_eq!(create_json["customAgentsLocalOnly"], false); + + let (resume_wire, _) = ResumeSessionConfig::new(SessionId::from("resume-locality")) + .with_custom_agents_local_only(false) + .into_wire() + .expect("resume config has no duplicate handlers"); + let resume_json = serde_json::to_value(&resume_wire).unwrap(); + assert_eq!(resume_json["customAgentsLocalOnly"], false); + + let (unset_create_wire, _) = SessionConfig::default() + .into_wire(Some(SessionId::from("create-unset"))) + .expect("create config has no duplicate handlers"); + let unset_create_json = serde_json::to_value(&unset_create_wire).unwrap(); + assert!(unset_create_json.get("customAgentsLocalOnly").is_none()); + + let (unset_resume_wire, _) = ResumeSessionConfig::new(SessionId::from("resume-unset")) + .into_wire() + .expect("resume config has no duplicate handlers"); + let unset_resume_json = serde_json::to_value(&unset_resume_wire).unwrap(); + assert!(unset_resume_json.get("customAgentsLocalOnly").is_none()); +} + +#[test] +fn session_config_enable_mcp_apps_sets_wire_flag_and_serializes() { + let cfg = SessionConfig::default().with_enable_mcp_apps(true); + assert_eq!(cfg.enable_mcp_apps, Some(true)); + + let (wire, _runtime) = cfg + .into_wire(Some(SessionId::from("enable-mcp-apps"))) + .expect("enable_mcp_apps config has no duplicate handlers"); + assert!(wire.request_mcp_apps); + + let json = serde_json::to_value(&wire).unwrap(); + assert_eq!(json["requestMcpApps"], serde_json::Value::Bool(true)); +} + +#[test] +fn resume_session_config_enable_mcp_apps_sets_wire_flag_and_serializes() { + let cfg = ResumeSessionConfig::new(SessionId::from("resume-enable-mcp-apps")) + .with_enable_mcp_apps(true); + assert_eq!(cfg.enable_mcp_apps, Some(true)); + + let (wire, _runtime) = cfg + .into_wire() + .expect("resume enable_mcp_apps config has no duplicate handlers"); + assert!(wire.request_mcp_apps); + + let json = serde_json::to_value(&wire).unwrap(); + assert_eq!(json["requestMcpApps"], serde_json::Value::Bool(true)); +} + +#[test] +fn github_mcp_tool_config_serializes_for_create_and_resume() { + let github_config = GitHubMcpToolConfig::new() + .with_enable_all_tools(true) + .with_additional_toolsets(["repos"]) + .with_additional_tools(["get_issue"]) + .with_enable_insiders_mode(true) + .with_disable_form_deferral(true); + + let (create_wire, _) = SessionConfig::default() + .with_github_mcp_tool_config(github_config.clone()) + .into_wire(Some(SessionId::from("github-mcp"))) + .expect("create config has no duplicate handlers"); + assert_eq!( + serde_json::to_value(&create_wire).unwrap()["githubMcpToolConfig"], + serde_json::json!({ + "enableAllTools": true, + "additionalToolsets": ["repos"], + "additionalTools": ["get_issue"], + "enableInsidersMode": true, + "disableFormDeferral": true, + }) + ); + + let (resume_wire, _) = ResumeSessionConfig::new(SessionId::from("github-mcp")) + .with_github_mcp_tool_config(github_config) + .into_wire() + .expect("resume config has no duplicate handlers"); + assert!(resume_wire.github_mcp_tool_config.is_some()); + + let (unset_wire, _) = SessionConfig::default() + .into_wire(Some(SessionId::from("github-mcp-unset"))) + .expect("default config has no duplicate handlers"); + assert!( + serde_json::to_value(&unset_wire) + .unwrap() + .get("githubMcpToolConfig") + .is_none() + ); +} + +#[test] +fn memory_configuration_constructors_and_serde() { + assert!(MemoryConfiguration::enabled().enabled); + assert!(!MemoryConfiguration::disabled().enabled); + assert!(MemoryConfiguration::disabled().with_enabled(true).enabled); + + let json = serde_json::to_value(MemoryConfiguration::enabled()).unwrap(); + assert_eq!(json, serde_json::json!({ "enabled": true })); +} + +#[test] +fn session_config_with_memory_serializes() { + let (wire, _runtime) = SessionConfig::default() + .with_memory(MemoryConfiguration::enabled()) + .into_wire(Some(SessionId::from("memory-on"))) + .expect("no duplicate handlers"); + let json = serde_json::to_value(&wire).unwrap(); + assert_eq!(json["memory"], serde_json::json!({ "enabled": true })); + + let (wire_off, _) = SessionConfig::default() + .with_memory(MemoryConfiguration::disabled()) + .into_wire(Some(SessionId::from("memory-off"))) + .expect("no duplicate handlers"); + let json_off = serde_json::to_value(&wire_off).unwrap(); + assert_eq!(json_off["memory"], serde_json::json!({ "enabled": false })); + + // Unset memory is omitted on the wire. + let (empty_wire, _) = SessionConfig::default() + .into_wire(Some(SessionId::from("memory-unset"))) + .expect("no duplicate handlers"); + let empty_json = serde_json::to_value(&empty_wire).unwrap(); + assert!(empty_json.get("memory").is_none()); +} + +#[test] +fn resume_session_config_with_memory_serializes() { + let (wire, _runtime) = ResumeSessionConfig::new(SessionId::from("resume-memory-on")) + .with_memory(MemoryConfiguration::enabled()) + .into_wire() + .expect("no duplicate handlers"); + let json = serde_json::to_value(&wire).unwrap(); + assert_eq!(json["memory"], serde_json::json!({ "enabled": true })); + + // Unset memory is omitted on the wire. + let (empty_wire, _) = ResumeSessionConfig::new(SessionId::from("resume-memory-unset")) + .into_wire() + .expect("no duplicate handlers"); + let empty_json = serde_json::to_value(&empty_wire).unwrap(); + assert!(empty_json.get("memory").is_none()); +} + +#[test] +fn feature_flags_serialize_on_create_and_resume() { + let feature_flags = HashMap::from([ + ("BACKGROUND_TASK_NOTIFICATION_PAYLOADS".to_string(), true), + ("DISABLED_TEST_FLAG".to_string(), false), + ]); + let expected = serde_json::json!({ + "BACKGROUND_TASK_NOTIFICATION_PAYLOADS": true, + "DISABLED_TEST_FLAG": false, + }); + + let create_config = SessionConfig::default().with_feature_flags(feature_flags.clone()); + assert_eq!(create_config.feature_flags.as_ref(), Some(&feature_flags)); + let (create_wire, _) = create_config + .into_wire(Some(SessionId::from("feature-flags-create"))) + .expect("no duplicate handlers"); + let create_json = serde_json::to_value(&create_wire).unwrap(); + assert_eq!(create_json["featureFlags"], expected); + + let (resume_wire, _) = ResumeSessionConfig::new(SessionId::from("feature-flags-resume")) + .with_feature_flags(feature_flags) + .into_wire() + .expect("no duplicate handlers"); + let resume_json = serde_json::to_value(&resume_wire).unwrap(); + assert_eq!(resume_json["featureFlags"], expected); + + let (unset_create_wire, _) = SessionConfig::default() + .into_wire(Some(SessionId::from("feature-flags-create-unset"))) + .expect("no duplicate handlers"); + let unset_create_json = serde_json::to_value(&unset_create_wire).unwrap(); + assert!(unset_create_json.get("featureFlags").is_none()); + + let (unset_resume_wire, _) = + ResumeSessionConfig::new(SessionId::from("feature-flags-resume-unset")) + .into_wire() + .expect("no duplicate handlers"); + let unset_resume_json = serde_json::to_value(&unset_resume_wire).unwrap(); + assert!(unset_resume_json.get("featureFlags").is_none()); +} + +fn sample_exp_assignments(context: &str) -> CopilotExpAssignmentResponse { + CopilotExpAssignmentResponse { + features: vec!["copilot_exp_flag".to_string()], + flights: HashMap::from([("copilot_exp_flag".to_string(), "treatment".to_string())]), + configs: vec![ExpConfigEntry { + id: "cfg-1".to_string(), + parameters: HashMap::from([ + ("threshold".to_string(), ExpFlagValue::Integer(5)), + ("enabled".to_string(), ExpFlagValue::Bool(true)), + ]), + }], + assignment_context: context.to_string(), + ..Default::default() + } +} + +#[test] +fn exp_flag_value_round_trips_all_variants() { + let values = serde_json::json!({ + "s": "text", + "i": 7, + "f": 1.5, + "b": true, + "n": null, + }); + let parsed: HashMap = serde_json::from_value(values.clone()).unwrap(); + assert_eq!(parsed["s"], ExpFlagValue::String("text".to_string())); + assert_eq!(parsed["i"], ExpFlagValue::Integer(7)); + assert_eq!(parsed["f"], ExpFlagValue::Float(1.5)); + assert_eq!(parsed["b"], ExpFlagValue::Bool(true)); + assert_eq!(parsed["n"], ExpFlagValue::Null); + assert_eq!(serde_json::to_value(&parsed).unwrap(), values); +} + +#[test] +fn session_config_with_exp_assignments_serializes() { + let assignments = sample_exp_assignments("ctx-123"); + let expected = serde_json::to_value(&assignments).unwrap(); + let (wire, _runtime) = SessionConfig::default() + .with_exp_assignments(assignments) + .into_wire(Some(SessionId::from("exp-on"))) + .expect("no duplicate handlers"); + let json = serde_json::to_value(&wire).unwrap(); + assert_eq!(json["expAssignments"], expected); + assert_eq!(json["expAssignments"]["AssignmentContext"], "ctx-123"); + assert_eq!( + json["expAssignments"]["Flights"]["copilot_exp_flag"], + "treatment" + ); + + // Unset exp assignments are omitted on the wire. + let (empty_wire, _) = SessionConfig::default() + .into_wire(Some(SessionId::from("exp-unset"))) + .expect("no duplicate handlers"); + let empty_json = serde_json::to_value(&empty_wire).unwrap(); + assert!(empty_json.get("expAssignments").is_none()); +} + +#[test] +fn resume_session_config_with_exp_assignments_serializes() { + let assignments = sample_exp_assignments("ctx-456"); + let expected = serde_json::to_value(&assignments).unwrap(); + let (wire, _runtime) = ResumeSessionConfig::new(SessionId::from("resume-exp-on")) + .with_exp_assignments(assignments) + .into_wire() + .expect("no duplicate handlers"); + let json = serde_json::to_value(&wire).unwrap(); + assert_eq!(json["expAssignments"], expected); + + // Unset exp assignments are omitted on the wire. + let (empty_wire, _) = ResumeSessionConfig::new(SessionId::from("resume-exp-unset")) + .into_wire() + .expect("no duplicate handlers"); + let empty_json = serde_json::to_value(&empty_wire).unwrap(); + assert!(empty_json.get("expAssignments").is_none()); +} + +#[test] +fn session_config_clone_preserves_exp_assignments() { + let assignments = sample_exp_assignments("ctx-clone"); + let config = SessionConfig::default().with_exp_assignments(assignments.clone()); + let cloned = config.clone(); + + assert_eq!(cloned.exp_assignments.as_ref(), Some(&assignments)); + + let (wire, _runtime) = cloned + .into_wire(Some(SessionId::from("exp-clone"))) + .expect("no duplicate handlers"); + let json = serde_json::to_value(&wire).unwrap(); + assert_eq!( + json["expAssignments"], + serde_json::to_value(&assignments).unwrap() + ); +} + +#[test] +fn resume_session_config_clone_preserves_exp_assignments() { + let assignments = sample_exp_assignments("ctx-clone-resume"); + let config = ResumeSessionConfig::new(SessionId::from("resume-exp-clone")) + .with_exp_assignments(assignments.clone()); + let cloned = config.clone(); + + assert_eq!(cloned.exp_assignments.as_ref(), Some(&assignments)); + + let (wire, _runtime) = cloned.into_wire().expect("no duplicate handlers"); + let json = serde_json::to_value(&wire).unwrap(); + assert_eq!( + json["expAssignments"], + serde_json::to_value(&assignments).unwrap() + ); +} + +#[test] +#[allow(clippy::field_reassign_with_default)] +fn session_config_into_wire_serializes_bucket_b_fields() { + use std::path::PathBuf; + + use super::{CloudSessionOptions, CloudSessionRepository}; + + let mut cfg = SessionConfig::default(); + cfg.config_directory = Some(PathBuf::from("/tmp/cfg")); + cfg.working_directory = Some(PathBuf::from("/tmp/work")); + cfg.github_token = Some("ghs_secret".to_string()); + cfg.include_sub_agent_streaming_events = Some(false); + cfg.enable_session_telemetry = Some(false); + cfg.reasoning_summary = Some(ReasoningSummary::Concise); + cfg.remote_session = Some(crate::generated::api_types::RemoteSessionMode::Export); + cfg.enable_on_demand_instruction_discovery = Some(false); + cfg.cloud = Some(CloudSessionOptions::with_repository( + CloudSessionRepository::new("github", "copilot-sdk").with_branch("main"), + )); + + let (wire, _runtime) = cfg + .into_wire(Some(SessionId::from("custom-id"))) + .expect("no duplicate handlers"); + let wire_json = serde_json::to_value(&wire).unwrap(); + assert_eq!(wire_json["sessionId"], "custom-id"); + assert_eq!(wire_json["configDir"], "/tmp/cfg"); + assert_eq!(wire_json["workingDirectory"], "/tmp/work"); + assert_eq!(wire_json["gitHubToken"], "ghs_secret"); + assert_eq!(wire_json["includeSubAgentStreamingEvents"], false); + assert_eq!(wire_json["enableSessionTelemetry"], false); + assert_eq!(wire_json["reasoningSummary"], "concise"); + assert_eq!(wire_json["remoteSession"], "export"); + assert_eq!(wire_json["enableOnDemandInstructionDiscovery"], false); + assert_eq!(wire_json["cloud"]["repository"]["owner"], "github"); + assert_eq!(wire_json["cloud"]["repository"]["name"], "copilot-sdk"); + assert_eq!(wire_json["cloud"]["repository"]["branch"], "main"); + + // Unset fields are omitted on the wire. + let (empty_wire, _) = SessionConfig::default() + .into_wire(Some(SessionId::from("empty"))) + .expect("default has no duplicate handlers"); + let empty_json = serde_json::to_value(&empty_wire).unwrap(); + assert!(empty_json.get("gitHubToken").is_none()); + assert!(empty_json.get("enableSessionTelemetry").is_none()); + assert!(empty_json.get("reasoningSummary").is_none()); + assert!(empty_json.get("remoteSession").is_none()); + assert!( + empty_json + .get("enableOnDemandInstructionDiscovery") + .is_none() + ); + assert!(empty_json.get("cloud").is_none()); +} + +#[test] +fn session_config_into_wire_serializes_named_providers_and_models() { + let cfg = SessionConfig::default() + .with_providers(vec![ + NamedProviderConfig::new("my-openai", "https://api.example.com/v1") + .with_provider_type("openai") + .with_wire_api("responses") + .with_model_provider("ollama") + .with_api_key("sk-test"), + ]) + .with_models(vec![ + ProviderModelConfig::new("gpt-x", "my-openai") + .with_wire_model("gpt-x-2025") + .with_max_output_tokens(2048), + ]); + + let (wire, _) = cfg + .into_wire(Some(SessionId::from("sess-providers"))) + .expect("no duplicate handlers"); + let wire_json = serde_json::to_value(&wire).unwrap(); + assert_eq!(wire_json["providers"][0]["name"], "my-openai"); + assert_eq!( + wire_json["providers"][0]["baseUrl"], + "https://api.example.com/v1" + ); + assert_eq!(wire_json["providers"][0]["type"], "openai"); + assert_eq!(wire_json["providers"][0]["wireApi"], "responses"); + assert_eq!(wire_json["providers"][0]["modelProvider"], "ollama"); + assert_eq!(wire_json["providers"][0]["apiKey"], "sk-test"); + assert_eq!(wire_json["models"][0]["id"], "gpt-x"); + assert_eq!(wire_json["models"][0]["provider"], "my-openai"); + assert_eq!(wire_json["models"][0]["wireModel"], "gpt-x-2025"); + assert_eq!(wire_json["models"][0]["maxOutputTokens"], 2048); + + let (empty_wire, _) = SessionConfig::default() + .into_wire(Some(SessionId::from("empty"))) + .expect("default has no duplicate handlers"); + let empty_json = serde_json::to_value(&empty_wire).unwrap(); + assert!(empty_json.get("providers").is_none()); + assert!(empty_json.get("models").is_none()); +} + +#[test] +fn resume_config_into_wire_serializes_named_providers_and_models() { + let cfg = ResumeSessionConfig::new(SessionId::from("sess-resume")) + .with_providers(vec![ + NamedProviderConfig::new("my-azure", "https://example.openai.azure.com") + .with_provider_type("azure") + .with_azure(AzureProviderOptions { + api_version: Some("2024-10-21".to_string()), + }), + ]) + .with_models(vec![ + ProviderModelConfig::new("deploy-1", "my-azure").with_model_id("gpt-4o"), + ]); + + let (wire, _) = cfg.into_wire().expect("no duplicate handlers"); + let wire_json = serde_json::to_value(&wire).unwrap(); + assert_eq!(wire_json["providers"][0]["name"], "my-azure"); + assert_eq!(wire_json["providers"][0]["type"], "azure"); + assert_eq!( + wire_json["providers"][0]["azure"]["apiVersion"], + "2024-10-21" + ); + assert_eq!(wire_json["models"][0]["id"], "deploy-1"); + assert_eq!(wire_json["models"][0]["provider"], "my-azure"); + assert_eq!(wire_json["models"][0]["modelId"], "gpt-4o"); + + let (empty_wire, _) = ResumeSessionConfig::new(SessionId::from("empty")) + .into_wire() + .expect("default has no duplicate handlers"); + let empty_json = serde_json::to_value(&empty_wire).unwrap(); + assert!(empty_json.get("providers").is_none()); + assert!(empty_json.get("models").is_none()); +} + +#[test] +fn session_config_into_wire_serializes_plugin_directories_and_large_output() { + use std::path::PathBuf; + + let cfg = SessionConfig { + plugin_directories: Some(vec![PathBuf::from("/tmp/plugins")]), + disabled_mcp_servers: Some(vec!["local-files".to_string(), "remote-github".to_string()]), + large_output: Some( + LargeToolOutputConfig::new() + .with_enabled(true) + .with_max_size_bytes(1024) + .with_output_directory(PathBuf::from("/tmp/large-output")), + ), + ..Default::default() + }; + + let (wire, _) = cfg + .into_wire(Some(SessionId::from("sess-1"))) + .expect("no duplicate handlers"); + let wire_json = serde_json::to_value(&wire).unwrap(); + assert_eq!(wire_json["pluginDirectories"][0], "/tmp/plugins"); + assert_eq!( + wire_json["disabledMcpServers"], + serde_json::json!(["local-files", "remote-github"]) + ); + assert_eq!(wire_json["largeOutput"]["enabled"], true); + assert_eq!(wire_json["largeOutput"]["maxSizeBytes"], 1024); + assert_eq!(wire_json["largeOutput"]["outputDir"], "/tmp/large-output"); + + let (empty_wire, _) = SessionConfig::default() + .into_wire(Some(SessionId::from("empty"))) + .expect("default has no duplicate handlers"); + let empty_json = serde_json::to_value(&empty_wire).unwrap(); + assert!(empty_json.get("pluginDirectories").is_none()); + assert!(empty_json.get("disabledMcpServers").is_none()); + assert!(empty_json.get("largeOutput").is_none()); +} + +#[test] +fn resume_session_config_into_wire_serializes_bucket_b_fields() { + use std::path::PathBuf; + + let mut cfg = ResumeSessionConfig::new(SessionId::from("sess-1")); + cfg.working_directory = Some(PathBuf::from("/tmp/work")); + cfg.config_directory = Some(PathBuf::from("/tmp/cfg")); + cfg.github_token = Some("ghs_secret".to_string()); + cfg.include_sub_agent_streaming_events = Some(true); + cfg.enable_session_telemetry = Some(false); + cfg.reasoning_summary = Some(ReasoningSummary::Detailed); + cfg.remote_session = Some(crate::generated::api_types::RemoteSessionMode::On); + cfg.enable_on_demand_instruction_discovery = Some(false); + + let (wire, _) = cfg.into_wire().expect("no duplicate handlers"); + let wire_json = serde_json::to_value(&wire).unwrap(); + assert_eq!(wire_json["sessionId"], "sess-1"); + assert_eq!(wire_json["workingDirectory"], "/tmp/work"); + assert_eq!(wire_json["configDir"], "/tmp/cfg"); + assert_eq!(wire_json["gitHubToken"], "ghs_secret"); + assert_eq!(wire_json["includeSubAgentStreamingEvents"], true); + assert_eq!(wire_json["enableSessionTelemetry"], false); + assert_eq!(wire_json["reasoningSummary"], "detailed"); + assert_eq!(wire_json["remoteSession"], "on"); + assert_eq!(wire_json["enableOnDemandInstructionDiscovery"], false); + + // Unset remote_session is omitted on the wire. + let (empty_wire, _) = ResumeSessionConfig::new(SessionId::from("sess-2")) + .into_wire() + .expect("default resume has no duplicate handlers"); + let empty_json = serde_json::to_value(&empty_wire).unwrap(); + assert!(empty_json.get("reasoningSummary").is_none()); + assert!(empty_json.get("remoteSession").is_none()); + assert!( + empty_json + .get("enableOnDemandInstructionDiscovery") + .is_none() + ); +} + +#[test] +fn resume_session_config_into_wire_serializes_plugin_directories_and_large_output() { + use std::path::PathBuf; + + let mut cfg = ResumeSessionConfig::new(SessionId::from("sess-1")); + cfg.plugin_directories = Some(vec![PathBuf::from("/tmp/plugins-r")]); + cfg.disabled_mcp_servers = Some(vec!["local-files-r".to_string()]); + cfg.large_output = Some( + LargeToolOutputConfig::new() + .with_enabled(false) + .with_max_size_bytes(2048) + .with_output_directory(PathBuf::from("/tmp/large-output-r")), + ); + + let (wire, _) = cfg.into_wire().expect("no duplicate handlers"); + let wire_json = serde_json::to_value(&wire).unwrap(); + assert_eq!(wire_json["pluginDirectories"][0], "/tmp/plugins-r"); + assert_eq!( + wire_json["disabledMcpServers"], + serde_json::json!(["local-files-r"]) + ); + assert_eq!(wire_json["largeOutput"]["enabled"], false); + assert_eq!(wire_json["largeOutput"]["maxSizeBytes"], 2048); + assert_eq!(wire_json["largeOutput"]["outputDir"], "/tmp/large-output-r"); + + let (empty_wire, _) = ResumeSessionConfig::new(SessionId::from("sess-2")) + .into_wire() + .expect("default resume has no duplicate handlers"); + let empty_json = serde_json::to_value(&empty_wire).unwrap(); + assert!(empty_json.get("pluginDirectories").is_none()); + assert!(empty_json.get("disabledMcpServers").is_none()); + assert!(empty_json.get("largeOutput").is_none()); +} + +#[test] +fn auth_client_id_metadata_url_reaches_create_and_resume_wire_payloads() { + let url = "https://example.com/oauth/client-metadata.json"; + + let (create_wire, _) = SessionConfig::default() + .with_auth_client_id_metadata_url(url) + .into_wire(None) + .expect("default create has no duplicate handlers"); + let create_json = serde_json::to_value(&create_wire).unwrap(); + assert_eq!(create_json["authClientIdMetadataUrl"], url); + + let (resume_wire, _) = ResumeSessionConfig::new(SessionId::from("sess-1")) + .with_auth_client_id_metadata_url(url) + .into_wire() + .expect("default resume has no duplicate handlers"); + let resume_json = serde_json::to_value(&resume_wire).unwrap(); + assert_eq!(resume_json["authClientIdMetadataUrl"], url); + + let (empty_create_wire, _) = SessionConfig::default() + .into_wire(None) + .expect("default create has no duplicate handlers"); + let empty_create_json = serde_json::to_value(&empty_create_wire).unwrap(); + assert!(empty_create_json.get("authClientIdMetadataUrl").is_none()); + + let (empty_resume_wire, _) = ResumeSessionConfig::new(SessionId::from("sess-2")) + .into_wire() + .expect("default resume has no duplicate handlers"); + let empty_resume_json = serde_json::to_value(&empty_resume_wire).unwrap(); + assert!(empty_resume_json.get("authClientIdMetadataUrl").is_none()); +} + +#[test] +fn session_config_clones_disabled_mcp_servers() { + let create = SessionConfig::default().with_disabled_mcp_servers(["local-files"]); + let mut create_clone = create.clone(); + create_clone + .disabled_mcp_servers + .as_mut() + .expect("configured disabled MCP servers") + .push("remote-github".to_string()); + assert_eq!( + create.disabled_mcp_servers.as_deref(), + Some(&["local-files".to_string()][..]) + ); + + let resume = ResumeSessionConfig::new(SessionId::from("sess-1")) + .with_disabled_mcp_servers(["local-files"]); + let mut resume_clone = resume.clone(); + resume_clone + .disabled_mcp_servers + .as_mut() + .expect("configured disabled MCP servers") + .push("remote-github".to_string()); + assert_eq!( + resume.disabled_mcp_servers.as_deref(), + Some(&["local-files".to_string()][..]) + ); +} + +#[test] +fn session_config_builder_composes() { + use indexmap::IndexMap; + + let cfg = SessionConfig::default() + .with_session_id(SessionId::from("sess-1")) + .with_model("claude-sonnet-4") + .with_client_name("test-app") + .with_reasoning_effort("medium") + .with_reasoning_summary(ReasoningSummary::Concise) + .with_context_tier("long_context") + .with_streaming(true) + .with_tools([Tool::new("greet")]) + .with_available_tools(["bash", "view"]) + .with_excluded_tools(["dangerous"]) + .with_mcp_servers(IndexMap::new()) + .with_mcp_oauth_token_storage("persistent") + .with_enable_config_discovery(true) + .with_enable_on_demand_instruction_discovery(true) + .with_skill_directories([PathBuf::from("/tmp/skills")]) + .with_disabled_skills(["broken-skill"]) + .with_disabled_mcp_servers(["local-files"]) + .with_agent("researcher") + .with_config_directory(PathBuf::from("/tmp/config")) + .with_working_directory(PathBuf::from("/tmp/work")) + .with_additional_directories([PathBuf::from("/tmp/shared")]) + .with_github_token("ghp_test") + .with_capi(CapiSessionOptions::new().with_enable_web_socket_responses(false)) + .with_enable_session_telemetry(false) + .with_include_sub_agent_streaming_events(false) + .with_extension_info(ExtensionInfo::new("github-app", "counter")); + + assert_eq!(cfg.session_id.as_ref().map(|s| s.as_str()), Some("sess-1")); + assert_eq!(cfg.model.as_deref(), Some("claude-sonnet-4")); + assert_eq!(cfg.client_name.as_deref(), Some("test-app")); + assert_eq!(cfg.reasoning_effort.as_deref(), Some("medium")); + assert_eq!(cfg.reasoning_summary, Some(ReasoningSummary::Concise)); + assert_eq!(cfg.context_tier.as_deref(), Some("long_context")); + assert_eq!(cfg.streaming, Some(true)); + assert_eq!(cfg.tools.as_ref().map(|t| t.len()), Some(1)); + assert_eq!( + cfg.available_tools.as_deref(), + Some(&["bash".to_string(), "view".to_string()][..]) + ); + assert_eq!( + cfg.excluded_tools.as_deref(), + Some(&["dangerous".to_string()][..]) + ); + assert!(cfg.mcp_servers.is_some()); + assert_eq!(cfg.mcp_oauth_token_storage.as_deref(), Some("persistent")); + assert_eq!(cfg.enable_config_discovery, Some(true)); + assert_eq!(cfg.enable_on_demand_instruction_discovery, Some(true)); + assert_eq!( + cfg.skill_directories.as_deref(), + Some(&[PathBuf::from("/tmp/skills")][..]) + ); + assert_eq!( + cfg.disabled_skills.as_deref(), + Some(&["broken-skill".to_string()][..]) + ); + assert_eq!( + cfg.disabled_mcp_servers.as_deref(), + Some(&["local-files".to_string()][..]) + ); + assert_eq!(cfg.agent.as_deref(), Some("researcher")); + assert_eq!(cfg.config_directory, Some(PathBuf::from("/tmp/config"))); + assert_eq!(cfg.working_directory, Some(PathBuf::from("/tmp/work"))); + assert_eq!( + cfg.additional_directories.as_deref(), + Some(&[PathBuf::from("/tmp/shared")][..]) + ); + assert_eq!(cfg.github_token.as_deref(), Some("ghp_test")); + assert_eq!( + cfg.capi, + Some(CapiSessionOptions::new().with_enable_web_socket_responses(false)) + ); + assert_eq!(cfg.enable_session_telemetry, Some(false)); + assert_eq!(cfg.include_sub_agent_streaming_events, Some(false)); + assert_eq!( + cfg.extension_info, + Some(ExtensionInfo::new("github-app", "counter")) + ); +} + +#[test] +fn resume_session_config_builder_composes() { + use indexmap::IndexMap; + + let cfg = ResumeSessionConfig::new(SessionId::from("sess-2")) + .with_client_name("test-app") + .with_reasoning_summary(ReasoningSummary::None) + .with_context_tier("default") + .with_streaming(true) + .with_tools([Tool::new("greet")]) + .with_available_tools(["bash", "view"]) + .with_excluded_tools(["dangerous"]) + .with_mcp_servers(IndexMap::new()) + .with_mcp_oauth_token_storage("persistent") + .with_enable_config_discovery(true) + .with_enable_on_demand_instruction_discovery(false) + .with_skill_directories([PathBuf::from("/tmp/skills")]) + .with_disabled_skills(["broken-skill"]) + .with_disabled_mcp_servers(["local-files"]) + .with_agent("researcher") + .with_config_directory(PathBuf::from("/tmp/config")) + .with_working_directory(PathBuf::from("/tmp/work")) + .with_additional_directories([PathBuf::from("/tmp/shared")]) + .with_github_token("ghp_test") + .with_capi(CapiSessionOptions::new().with_enable_web_socket_responses(false)) + .with_enable_session_telemetry(false) + .with_include_sub_agent_streaming_events(true) + .with_suppress_resume_event(true) + .with_continue_pending_work(true) + .with_extension_info(ExtensionInfo::new("github-app", "counter")); + + assert_eq!(cfg.session_id.as_str(), "sess-2"); + assert_eq!(cfg.client_name.as_deref(), Some("test-app")); + assert_eq!(cfg.reasoning_summary, Some(ReasoningSummary::None)); + assert_eq!(cfg.context_tier.as_deref(), Some("default")); + assert_eq!(cfg.streaming, Some(true)); + assert_eq!(cfg.tools.as_ref().map(|t| t.len()), Some(1)); + assert_eq!( + cfg.available_tools.as_deref(), + Some(&["bash".to_string(), "view".to_string()][..]) + ); + assert_eq!( + cfg.excluded_tools.as_deref(), + Some(&["dangerous".to_string()][..]) + ); + assert!(cfg.mcp_servers.is_some()); + assert_eq!(cfg.mcp_oauth_token_storage.as_deref(), Some("persistent")); + assert_eq!(cfg.enable_config_discovery, Some(true)); + assert_eq!(cfg.enable_on_demand_instruction_discovery, Some(false)); + assert_eq!( + cfg.skill_directories.as_deref(), + Some(&[PathBuf::from("/tmp/skills")][..]) + ); + assert_eq!( + cfg.disabled_skills.as_deref(), + Some(&["broken-skill".to_string()][..]) + ); + assert_eq!( + cfg.disabled_mcp_servers.as_deref(), + Some(&["local-files".to_string()][..]) + ); + assert_eq!(cfg.agent.as_deref(), Some("researcher")); + assert_eq!(cfg.config_directory, Some(PathBuf::from("/tmp/config"))); + assert_eq!(cfg.working_directory, Some(PathBuf::from("/tmp/work"))); + assert_eq!( + cfg.additional_directories.as_deref(), + Some(&[PathBuf::from("/tmp/shared")][..]) + ); + assert_eq!(cfg.github_token.as_deref(), Some("ghp_test")); + assert_eq!( + cfg.capi, + Some(CapiSessionOptions::new().with_enable_web_socket_responses(false)) + ); + assert_eq!(cfg.enable_session_telemetry, Some(false)); + assert_eq!(cfg.include_sub_agent_streaming_events, Some(true)); + assert_eq!(cfg.suppress_resume_event, Some(true)); + assert_eq!(cfg.continue_pending_work, Some(true)); + assert_eq!( + cfg.extension_info, + Some(ExtensionInfo::new("github-app", "counter")) + ); +} + +/// `continue_pending_work` must serialize to wire as `continuePendingWork` +/// — the runtime keys off this exact field name to opt into the +/// pending-work-handoff pattern. +#[test] +fn resume_session_config_serializes_continue_pending_work_to_camel_case() { + let cfg = ResumeSessionConfig::new(SessionId::from("sess-1")).with_continue_pending_work(true); + let (wire, _) = cfg.into_wire().expect("no duplicate handlers"); + let json = serde_json::to_value(&wire).unwrap(); + assert_eq!(json["continuePendingWork"], true); + + // Unset case — skip_serializing_if must omit the field. + let (wire, _) = ResumeSessionConfig::new(SessionId::from("sess-2")) + .into_wire() + .expect("no duplicate handlers"); + let json = serde_json::to_value(&wire).unwrap(); + assert!(json.get("continuePendingWork").is_none()); +} + +#[test] +fn resume_policy_and_recovery_report_round_trip() { + let config = + ResumeSessionConfig::new(SessionId::from("sess-1")).with_allow_transcript_recovery(false); + let (wire, _) = config.into_wire().unwrap(); + let value = serde_json::to_value(&wire).unwrap(); + assert_eq!(value["allowTranscriptRecovery"], false); + + let (wire, _) = ResumeSessionConfig::new(SessionId::from("sess-2")) + .into_wire() + .unwrap(); + assert!( + serde_json::to_value(&wire) + .unwrap() + .get("allowTranscriptRecovery") + .is_none() + ); + + let result: crate::types::ResumeSessionResult = serde_json::from_value(serde_json::json!({ + "sessionId": "sess-1", + "transcriptRecovery": { + "plannedBackupPath": "events.jsonl.backup", + "invalidLineNumbers": [2], + "sessionStartMoved": false + } + })) + .unwrap(); + let recovery = result.transcript_recovery.unwrap(); + assert_eq!(recovery.invalid_line_numbers, vec![2]); + assert_eq!(recovery.planned_backup_path, "events.jsonl.backup"); +} + +#[test] +fn session_configs_serialize_additional_directories() { + let create = SessionConfig::default().with_additional_directories([ + PathBuf::from("/tmp/shared"), + PathBuf::from("/tmp/generated"), + ]); + let (create_wire, _) = create.into_wire(None).expect("no duplicate handlers"); + let create_json = serde_json::to_value(&create_wire).unwrap(); + assert_eq!( + create_json["additionalDirectories"], + serde_json::json!(["/tmp/shared", "/tmp/generated"]) + ); + + let resume = ResumeSessionConfig::new(SessionId::from("sess-1")) + .with_additional_directories([PathBuf::from("/tmp/resumed")]); + let (resume_wire, _) = resume.into_wire().expect("no duplicate handlers"); + let resume_json = serde_json::to_value(&resume_wire).unwrap(); + assert_eq!( + resume_json["additionalDirectories"], + serde_json::json!(["/tmp/resumed"]) + ); +} + +/// The Rust field is `suppress_resume_event`, but the wire field stays +/// `disableResume` to preserve compatibility with the runtime and other +/// SDKs. +#[test] +fn resume_session_config_serializes_suppress_resume_event_to_disable_resume_on_wire() { + let cfg = ResumeSessionConfig::new(SessionId::from("sess-1")).with_suppress_resume_event(true); + let (wire, _) = cfg.into_wire().expect("no duplicate handlers"); + let json = serde_json::to_value(&wire).unwrap(); + assert_eq!(json["disableResume"], true); + assert!(json.get("suppressResumeEvent").is_none()); +} + +/// `instruction_directories` must serialize to wire as +/// `instructionDirectories` on `SessionConfig`. +#[test] +fn session_config_serializes_instruction_directories_to_camel_case() { + let cfg = SessionConfig::default().with_instruction_directories([PathBuf::from("/tmp/instr")]); + let (wire, _) = cfg + .into_wire(Some(SessionId::from("instr-on"))) + .expect("no duplicate handlers"); + let json = serde_json::to_value(&wire).unwrap(); + assert_eq!( + json["instructionDirectories"], + serde_json::json!(["/tmp/instr"]) + ); + + // Unset case — skip_serializing_if must omit the field. + let (wire, _) = SessionConfig::default() + .into_wire(Some(SessionId::from("instr-off"))) + .expect("no duplicate handlers"); + let json = serde_json::to_value(&wire).unwrap(); + assert!(json.get("instructionDirectories").is_none()); +} + +/// Same check on the resume path. Forwarded to the CLI on +/// `session.resume`. +#[test] +fn resume_session_config_serializes_instruction_directories_to_camel_case() { + let cfg = ResumeSessionConfig::new(SessionId::from("sess-1")) + .with_instruction_directories([PathBuf::from("/tmp/instr")]); + let (wire, _) = cfg.into_wire().expect("no duplicate handlers"); + let json = serde_json::to_value(&wire).unwrap(); + assert_eq!( + json["instructionDirectories"], + serde_json::json!(["/tmp/instr"]) + ); + + let (wire, _) = ResumeSessionConfig::new(SessionId::from("sess-2")) + .into_wire() + .expect("no duplicate handlers"); + let json = serde_json::to_value(&wire).unwrap(); + assert!(json.get("instructionDirectories").is_none()); +} + +#[test] +fn custom_agent_config_builder_composes() { + use indexmap::IndexMap; + + let cfg = CustomAgentConfig::new("researcher", "You are a research assistant.") + .with_display_name("Research Assistant") + .with_description("Investigates technical questions.") + .with_tools(["bash", "view"]) + .with_mcp_servers(IndexMap::new()) + .with_infer(true) + .with_skills(["rust-coding-skill"]); + + assert_eq!(cfg.name, "researcher"); + assert_eq!(cfg.prompt, "You are a research assistant."); + assert_eq!(cfg.display_name.as_deref(), Some("Research Assistant")); + assert_eq!( + cfg.description.as_deref(), + Some("Investigates technical questions.") + ); + assert_eq!( + cfg.tools.as_deref(), + Some(&["bash".to_string(), "view".to_string()][..]) + ); + assert!(cfg.mcp_servers.is_some()); + assert_eq!(cfg.infer, Some(true)); + assert_eq!( + cfg.skills.as_deref(), + Some(&["rust-coding-skill".to_string()][..]) + ); +} + +#[test] +fn mcp_servers_serialize_in_insertion_order() { + use indexmap::IndexMap; + + // Regression: `mcp_servers` was a `HashMap`, so the server keys (and + // thus the `session.create` payload) serialized in a per-process + // random order; `IndexMap` pins them to insertion order. The long + // sequence makes a `HashMap` regression reproduce this exact order by + // chance only 1/N!, avoiding a flaky false pass. + let order = [ + "zebra", "quartz", "delta", "ivy", "mango", "bravo", "xenon", "amber", "falcon", "ceres", + "nova", "kelp", "otter", "yodel", "plum", "garnet", + ]; + let mut servers = IndexMap::new(); + for name in order { + servers.insert( + name.to_string(), + McpServerConfig::Stdio(McpStdioServerConfig { + command: "run".to_string(), + ..Default::default() + }), + ); + } + + let (wire, _runtime) = SessionConfig::default() + .with_mcp_servers(servers) + .into_wire(None) + .expect("into_wire should succeed"); + let json = serde_json::to_string(&wire).expect("serialize wire"); + + let positions: Vec = order + .iter() + .map(|name| { + json.find(&format!("\"{name}\"")) + .unwrap_or_else(|| panic!("server {name} missing from wire JSON")) + }) + .collect(); + let mut ascending = positions.clone(); + ascending.sort_unstable(); + assert_eq!( + positions, ascending, + "mcp server keys must serialize in insertion order: {json}" + ); +} + +#[test] +fn infinite_session_config_builder_composes() { + let cfg = InfiniteSessionConfig::new() + .with_enabled(true) + .with_background_compaction_threshold(0.75) + .with_buffer_exhaustion_threshold(0.92); + + assert_eq!(cfg.enabled, Some(true)); + assert_eq!(cfg.background_compaction_threshold, Some(0.75)); + assert_eq!(cfg.buffer_exhaustion_threshold, Some(0.92)); +} + +#[test] +fn provider_config_builder_composes() { + use std::collections::HashMap; + + let mut headers = HashMap::new(); + headers.insert("X-Custom".to_string(), "value".to_string()); + + let cfg = ProviderConfig::new("https://api.example.com") + .with_provider_type("openai") + .with_wire_api("completions") + .with_transport("websockets") + .with_model_provider("lm_studio") + .with_api_key("sk-test") + .with_bearer_token("bearer-test") + .with_headers(headers) + .with_model_id("gpt-4") + .with_wire_model("azure-gpt-4-deployment") + .with_max_prompt_tokens(8192) + .with_max_output_tokens(2048); + + assert_eq!(cfg.base_url, "https://api.example.com"); + assert_eq!(cfg.provider_type.as_deref(), Some("openai")); + assert_eq!(cfg.wire_api.as_deref(), Some("completions")); + assert_eq!(cfg.transport.as_deref(), Some("websockets")); + assert_eq!(cfg.model_provider.as_deref(), Some("lm_studio")); + assert_eq!(cfg.api_key.as_deref(), Some("sk-test")); + assert_eq!(cfg.bearer_token.as_deref(), Some("bearer-test")); + assert_eq!( + cfg.headers + .as_ref() + .and_then(|h| h.get("X-Custom")) + .map(String::as_str), + Some("value"), + ); + assert_eq!(cfg.model_id.as_deref(), Some("gpt-4")); + assert_eq!(cfg.wire_model.as_deref(), Some("azure-gpt-4-deployment")); + assert_eq!(cfg.max_prompt_tokens, Some(8192)); + assert_eq!(cfg.max_output_tokens, Some(2048)); + + // Wire-shape: camelCase, skip_serializing_if when unset. + let wire = serde_json::to_value(&cfg).unwrap(); + assert_eq!(wire["modelId"], "gpt-4"); + assert_eq!(wire["wireModel"], "azure-gpt-4-deployment"); + assert_eq!(wire["modelProvider"], "lm_studio"); + assert_eq!(wire["maxPromptTokens"], 8192); + assert_eq!(wire["maxOutputTokens"], 2048); + + let unset = ProviderConfig::new("https://api.example.com"); + let wire_unset = serde_json::to_value(&unset).unwrap(); + assert!(wire_unset.get("modelId").is_none()); + assert!(wire_unset.get("wireModel").is_none()); + assert!(wire_unset.get("modelProvider").is_none()); + assert!(wire_unset.get("maxPromptTokens").is_none()); + assert!(wire_unset.get("maxOutputTokens").is_none()); +} + +#[test] +fn capi_session_options_builder_composes_and_serializes() { + let cfg = CapiSessionOptions::new().with_enable_web_socket_responses(false); + + assert_eq!(cfg.enable_web_socket_responses, Some(false)); + + let wire = serde_json::to_value(&cfg).unwrap(); + assert_eq!( + wire, + serde_json::json!({ "enableWebSocketResponses": false }) + ); + + let unset = CapiSessionOptions::new(); + let wire_unset = serde_json::to_value(&unset).unwrap(); + assert!(wire_unset.get("enableWebSocketResponses").is_none()); + assert!(wire_unset.get("autoTier").is_none()); + assert_eq!(wire_unset, json!({})); +} + +#[test] +fn capi_auto_tier_canonical_values_round_trip_and_forward() { + for (tier, value) in [ + (AutoTier::Efficiency, "efficiency"), + (AutoTier::Balance, "balance"), + (AutoTier::Intelligence, "intelligence"), + (AutoTier::Fast, "fast"), + ] { + let exported: crate::AutoTier = tier.clone(); + let capi = CapiSessionOptions::new().with_auto_tier(exported); + assert_eq!(capi.auto_tier, Some(tier)); + assert_eq!( + serde_json::to_value(&capi).unwrap(), + json!({"autoTier": value}) + ); + assert_eq!( + serde_json::from_value::(json!({"autoTier": value})).unwrap(), + capi + ); + + let capi = capi.with_enable_web_socket_responses(false); + let expected = json!({"autoTier": value, "enableWebSocketResponses": false}); + let (create, _) = SessionConfig::default() + .with_model("auto") + .with_capi(capi.clone()) + .into_wire(Some(SessionId::from("capi-create"))) + .unwrap(); + assert_eq!(serde_json::to_value(create).unwrap()["capi"], expected); + + let (resume, _) = ResumeSessionConfig::new(SessionId::from("capi-resume")) + .with_capi(capi) + .into_wire() + .unwrap(); + assert_eq!(serde_json::to_value(resume).unwrap()["capi"], expected); + } +} + +#[test] +fn capi_auto_tier_accepts_unknown_values_for_forward_compatibility() { + for value in ["balanced", "Balance", "unknown"] { + assert_eq!( + serde_json::from_value::(json!(value)).unwrap(), + AutoTier::Unknown + ); + } + let capi: CapiSessionOptions = serde_json::from_value(json!({})).unwrap(); + assert_eq!(capi.auto_tier, None); +} + +#[test] +fn session_config_with_capi_serializes() { + let (wire, _) = SessionConfig::default() + .with_capi(CapiSessionOptions::new().with_enable_web_socket_responses(false)) + .into_wire(Some(SessionId::from("capi-create"))) + .expect("no duplicate handlers"); + let json = serde_json::to_value(&wire).unwrap(); + assert_eq!( + json["capi"], + serde_json::json!({ "enableWebSocketResponses": false }) + ); + + let (empty_wire, _) = SessionConfig::default() + .into_wire(Some(SessionId::from("capi-create-unset"))) + .expect("no duplicate handlers"); + let empty_json = serde_json::to_value(&empty_wire).unwrap(); + assert!(empty_json.get("capi").is_none()); +} + +#[test] +fn resume_session_config_with_capi_serializes() { + let (wire, _) = ResumeSessionConfig::new(SessionId::from("capi-resume")) + .with_capi(CapiSessionOptions::new().with_enable_web_socket_responses(false)) + .into_wire() + .expect("no duplicate handlers"); + let json = serde_json::to_value(&wire).unwrap(); + assert_eq!( + json["capi"], + serde_json::json!({ "enableWebSocketResponses": false }) + ); + + let (empty_wire, _) = ResumeSessionConfig::new(SessionId::from("capi-resume-unset")) + .into_wire() + .expect("no duplicate handlers"); + let empty_json = serde_json::to_value(&empty_wire).unwrap(); + assert!(empty_json.get("capi").is_none()); +} + +#[test] +fn system_message_config_builder_composes() { + use std::collections::HashMap; + + let cfg = SystemMessageConfig::new() + .with_mode("replace") + .with_content("Custom system message.") + .with_sections(HashMap::new()); + + assert_eq!(cfg.mode.as_deref(), Some("replace")); + assert_eq!(cfg.content.as_deref(), Some("Custom system message.")); + assert!(cfg.sections.is_some()); +} + +#[test] +fn delivery_mode_serializes_to_kebab_case_strings() { + assert_eq!( + serde_json::to_string(&DeliveryMode::Enqueue).unwrap(), + "\"enqueue\"" + ); + assert_eq!( + serde_json::to_string(&DeliveryMode::Immediate).unwrap(), + "\"immediate\"" + ); + let parsed: DeliveryMode = serde_json::from_str("\"immediate\"").unwrap(); + assert_eq!(parsed, DeliveryMode::Immediate); +} + +#[test] +fn agent_mode_serializes_to_kebab_case_strings() { + assert_eq!( + serde_json::to_string(&AgentMode::Interactive).unwrap(), + "\"interactive\"" + ); + assert_eq!(serde_json::to_string(&AgentMode::Plan).unwrap(), "\"plan\""); + assert_eq!( + serde_json::to_string(&AgentMode::Autopilot).unwrap(), + "\"autopilot\"" + ); + assert_eq!( + serde_json::to_string(&AgentMode::Shell).unwrap(), + "\"shell\"" + ); + let parsed: AgentMode = serde_json::from_str("\"plan\"").unwrap(); + assert_eq!(parsed, AgentMode::Plan); +} + +#[test] +fn connection_state_distinguishes_variants() { + // ConnectionState is now an internal type; verify we can construct + // and compare the variants used by the lifecycle code paths. + assert_ne!(ConnectionState::Connected, ConnectionState::Disconnected); +} + +/// `agentId` is the sub-agent attribution field added in copilot-sdk +/// commit f8cf846 ("Derive session event envelopes from schema"). +/// Every other SDK (Node, Python, Go, .NET) carries it on the event +/// envelope; Rust must too or sub-agent events lose attribution at +/// the deserialization boundary. Cross-SDK parity test. +#[test] +fn session_event_round_trips_agent_id_on_envelope() { + let wire = json!({ + "id": "evt-1", + "timestamp": "2026-04-30T12:00:00Z", + "parentId": null, + "agentId": "sub-agent-42", + "type": "assistant.message", + "data": { "message": "hi" } + }); + + let event: SessionEvent = serde_json::from_value(wire.clone()).unwrap(); + assert_eq!(event.agent_id.as_deref(), Some("sub-agent-42")); + + // Round-trip preserves the field on the wire. + let roundtripped = serde_json::to_value(&event).unwrap(); + assert_eq!(roundtripped["agentId"], "sub-agent-42"); + + // Absent agentId remains absent (skip_serializing_if). + let main_agent_event: SessionEvent = serde_json::from_value(json!({ + "id": "evt-2", + "timestamp": "2026-04-30T12:00:01Z", + "parentId": null, + "type": "session.idle", + "data": {} + })) + .unwrap(); + assert!(main_agent_event.agent_id.is_none()); + let roundtripped = serde_json::to_value(&main_agent_event).unwrap(); + assert!(roundtripped.get("agentId").is_none()); +} + +/// Same parity for the typed event envelope produced by the codegen. +#[test] +fn typed_session_event_round_trips_agent_id_on_envelope() { + let wire = json!({ + "id": "evt-1", + "timestamp": "2026-04-30T12:00:00Z", + "parentId": null, + "agentId": "sub-agent-42", + "type": "session.idle", + "data": {} + }); + + let event: TypedSessionEvent = serde_json::from_value(wire).unwrap(); + assert_eq!(event.agent_id.as_deref(), Some("sub-agent-42")); + + let roundtripped = serde_json::to_value(&event).unwrap(); + assert_eq!(roundtripped["agentId"], "sub-agent-42"); +} + +#[test] +fn connection_state_variants_compile() { + // Defensive smoke test: all variants must be constructable from + // within the crate. (The enum was demoted from pub to pub(crate) + // in Phase D; this test guards against accidental removal.) + let _ = ConnectionState::Disconnected; + let _ = ConnectionState::Connecting; + let _ = ConnectionState::Connected; + let _ = ConnectionState::Error; +} + +#[test] +fn deserializes_runtime_attachment_variants() { + let attachments: Vec = serde_json::from_value(json!([ + { + "type": "file", + "path": "/tmp/file.rs", + "displayName": "file.rs", + "lineRange": { "start": 7, "end": 12 } + }, + { + "type": "directory", + "path": "/tmp/project", + "displayName": "project" + }, + { + "type": "selection", + "filePath": "/tmp/lib.rs", + "displayName": "lib.rs", + "text": "fn main() {}", + "selection": { + "start": { "line": 1, "character": 2 }, + "end": { "line": 3, "character": 4 } + } + }, + { + "type": "blob", + "data": "Zm9v", + "mimeType": "image/png", + "displayName": "image.png" + }, + { + "type": "github_reference", + "number": 42, + "title": "Fix rendering", + "referenceType": "issue", + "state": "open", + "url": "https://github.com/example/repo/issues/42" + }, + { + "type": "extension_context", + "capturedAt": "2026-09-18T11:00:00Z", + "extensionId": "example:extension", + "title": "Unbound context" + } + ])) + .expect("attachments should deserialize"); + + assert_eq!(attachments.len(), 6); + assert!(matches!( + &attachments[0], + Attachment::File { + path, + display_name, + line_range: Some(AttachmentLineRange { start: 7, end: 12 }), + } if path == &PathBuf::from("/tmp/file.rs") && display_name.as_deref() == Some("file.rs") + )); + assert!(matches!( + &attachments[1], + Attachment::Directory { path, display_name } + if path == &PathBuf::from("/tmp/project") && display_name.as_deref() == Some("project") + )); + assert!(matches!( + &attachments[2], + Attachment::Selection { + file_path, + display_name, + selection: + AttachmentSelectionRange { + start: AttachmentSelectionPosition { line: 1, character: 2 }, + end: AttachmentSelectionPosition { line: 3, character: 4 }, + }, + .. + } if file_path == &PathBuf::from("/tmp/lib.rs") && display_name.as_deref() == Some("lib.rs") + )); + assert!(matches!( + &attachments[3], + Attachment::Blob { + data, + mime_type, + display_name, + } if data == "Zm9v" && mime_type == "image/png" && display_name.as_deref() == Some("image.png") + )); + assert!(matches!( + &attachments[4], + Attachment::GitHubReference { + number: 42, + title, + reference_type: GitHubReferenceType::Issue, + state, + url, + } if title == "Fix rendering" + && state == "open" + && url == "https://github.com/example/repo/issues/42" + )); + assert!(matches!( + &attachments[5], + Attachment::ExtensionContext { + captured_at, + extension_id, + canvas_id: None, + instance_id: None, + title, + payload: None, + } if captured_at == "2026-09-18T11:00:00Z" + && extension_id == "example:extension" + && title == "Unbound context" + )); + assert_eq!( + serde_json::to_value(&attachments[5]).expect("serialize extension context"), + json!({ + "type": "extension_context", + "capturedAt": "2026-09-18T11:00:00Z", + "extensionId": "example:extension", + "title": "Unbound context" + }) + ); +} + +#[test] +fn ensures_display_names_for_variants_that_support_them() { + let mut attachments = vec![ + Attachment::File { + path: PathBuf::from("/tmp/file.rs"), + display_name: None, + line_range: None, + }, + Attachment::Selection { + file_path: PathBuf::from("/tmp/src/lib.rs"), + display_name: None, + text: "fn main() {}".to_string(), + selection: AttachmentSelectionRange { + start: AttachmentSelectionPosition { + line: 0, + character: 0, + }, + end: AttachmentSelectionPosition { + line: 0, + character: 10, + }, + }, + }, + Attachment::Blob { + data: "Zm9v".to_string(), + mime_type: "image/png".to_string(), + display_name: None, + }, + Attachment::GitHubReference { + number: 7, + title: "Track regressions".to_string(), + reference_type: GitHubReferenceType::Issue, + state: "open".to_string(), + url: "https://example.com/issues/7".to_string(), + }, + ]; + + ensure_attachment_display_names(&mut attachments); + + assert_eq!(attachments[0].display_name(), Some("file.rs")); + assert_eq!(attachments[1].display_name(), Some("lib.rs")); + assert_eq!(attachments[2].display_name(), Some("attachment")); + assert_eq!(attachments[3].display_name(), None); + assert_eq!( + attachments[3].label(), + Some("Track regressions".to_string()) + ); +} + +#[test] +fn github_anchored_attachment_variants_round_trip() { + let cases = vec![ + ( + "github_commit", + json!({ + "type": "github_commit", + "message": "Fix the thing", + "oid": "abc123", + "repo": { "id": 1, "name": "repo", "owner": "octocat" }, + "url": "https://github.com/octocat/repo/commit/abc123" + }), + ), + ( + "github_release", + json!({ + "type": "github_release", + "name": "v1.2.3", + "repo": { "name": "repo", "owner": "octocat" }, + "tagName": "v1.2.3", + "url": "https://github.com/octocat/repo/releases/tag/v1.2.3" + }), + ), + ( + "github_actions_job", + json!({ + "type": "github_actions_job", + "conclusion": "failure", + "jobId": 99, + "jobName": "build", + "repo": { "name": "repo", "owner": "octocat" }, + "url": "https://github.com/octocat/repo/actions/runs/1/job/99", + "workflowName": "CI" + }), + ), + ( + "github_repository", + json!({ + "type": "github_repository", + "description": "An example repository", + "ref": "main", + "repo": { "name": "repo", "owner": "octocat" }, + "url": "https://github.com/octocat/repo" + }), + ), + ( + "github_file_diff", + json!({ + "type": "github_file_diff", + "base": { + "path": "src/lib.rs", + "ref": "main", + "repo": { "name": "repo", "owner": "octocat" } + }, + "head": { + "path": "src/lib.rs", + "ref": "feature", + "repo": { "name": "repo", "owner": "octocat" } + }, + "url": "https://github.com/octocat/repo/compare/main...feature" + }), + ), + ( + "github_tree_comparison", + json!({ + "type": "github_tree_comparison", + "base": { + "repo": { "name": "repo", "owner": "octocat" }, + "revision": "main" + }, + "head": { + "repo": { "name": "repo", "owner": "octocat" }, + "revision": "feature" + }, + "url": "https://github.com/octocat/repo/compare/main...feature" + }), + ), + ( + "github_url", + json!({ + "type": "github_url", + "url": "https://github.com/octocat/repo/wiki" + }), + ), + ( + "github_file", + json!({ + "type": "github_file", + "path": "src/main.rs", + "ref": "main", + "repo": { "name": "repo", "owner": "octocat" }, + "url": "https://github.com/octocat/repo/blob/main/src/main.rs" + }), + ), + ( + "github_snippet", + json!({ + "type": "github_snippet", + "lineRange": { "start": 10, "end": 20 }, + "path": "src/main.rs", + "ref": "main", + "repo": { "name": "repo", "owner": "octocat" }, + "url": "https://github.com/octocat/repo/blob/main/src/main.rs#L10-L20" + }), + ), + ]; + + for (expected_type, input) in cases { + let attachment: Attachment = serde_json::from_value(input.clone()) + .unwrap_or_else(|err| panic!("{expected_type} should deserialize: {err}")); + + // Serialize to a string first: parsing into `serde_json::Value` would + // silently dedupe a duplicate `type` key, hiding the exact regression + // this test guards against (e.g. a wrapped generated struct emitting its + // own `type` alongside the enum tag). + let serialized_string = serde_json::to_string(&attachment) + .unwrap_or_else(|err| panic!("{expected_type} should serialize: {err}")); + + // Exactly one `type` key, carrying the expected discriminator. + assert_eq!( + serialized_string.matches("\"type\":").count(), + 1, + "{expected_type} must serialize a single `type` key" + ); + + let serialized: serde_json::Value = serde_json::from_str(&serialized_string) + .unwrap_or_else(|err| panic!("{expected_type} should reparse: {err}")); + assert_eq!( + serialized.get("type").and_then(|value| value.as_str()), + Some(expected_type), + "{expected_type} must serialize the correct discriminator" + ); + + // Round-trips without dropping fields. + assert_eq!( + serialized, input, + "{expected_type} should round-trip without data loss" + ); + let reparsed: Attachment = serde_json::from_value(serialized) + .unwrap_or_else(|err| panic!("{expected_type} should re-deserialize: {err}")); + assert_eq!( + reparsed, attachment, + "{expected_type} should re-deserialize to the same value" + ); + } +} diff --git a/rust/tests/api_types_test.rs b/rust/tests/api_types_test.rs index 326de092db..fa714a7789 100644 --- a/rust/tests/api_types_test.rs +++ b/rust/tests/api_types_test.rs @@ -8,6 +8,7 @@ use github_copilot_sdk::rpc::{ ConnectorCapabilities, ConnectorCatalogEntry, ConnectorCatalogStatus, ConnectorConnectRequest, ConnectorConnectResult, ConnectorContinueRequest, ConnectorReconcileOptions, ConnectorReconcileRequest, ConnectorSessionAccount, ConnectorSessionAccountResult, + CustomizationReloadOutcome, CustomizationReloadStatus, CustomizationReloadSubsystem, EnqueueCommandResult, Extension, ExtensionList, ExtensionSource, ExtensionStatus, ExtensionsDisableRequest, ExtensionsEnableRequest, FleetStartRequest, FleetStartResult, McpDisableRequest, McpEnableOptions, McpEnableRequest, McpInstallationOperationStatus, @@ -22,6 +23,35 @@ use github_copilot_sdk::session_events::{ }; use github_copilot_sdk::{AutoTier, AutoTierPreference, SetModelOptions}; +#[test] +fn customization_reload_outcome_preserves_future_wire_values() { + let wire = serde_json::json!({ + "status": "future-status", + "subsystem": "future-subsystem", + "detail": "new component", + }); + let outcome: CustomizationReloadOutcome = serde_json::from_value(wire.clone()).unwrap(); + assert_eq!(serde_json::to_value(outcome).unwrap(), wire); + + assert_eq!( + serde_json::from_value::(serde_json::json!("reloaded")).unwrap(), + CustomizationReloadStatus::Reloaded + ); + assert_eq!( + serde_json::from_value::(serde_json::json!("skills")) + .unwrap(), + CustomizationReloadSubsystem::Skills + ); + assert_eq!( + serde_json::to_value(CustomizationReloadStatus::Reloaded).unwrap(), + serde_json::json!("reloaded") + ); + assert_eq!( + serde_json::to_value(CustomizationReloadSubsystem::Skills).unwrap(), + serde_json::json!("skills") + ); +} + #[test] fn operation_status_preserves_required_phases_and_original_identity() { for phase in [ diff --git a/rust/tests/e2e/session_fs.rs b/rust/tests/e2e/session_fs.rs index 8b13789179..479a07d935 100644 --- a/rust/tests/e2e/session_fs.rs +++ b/rust/tests/e2e/session_fs.rs @@ -1 +1,324 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +use std::path::{Path, PathBuf}; +use std::sync::{Arc, Mutex}; + +use async_trait::async_trait; +use base64::Engine; +use github_copilot_sdk::session_fs::{FsError, SessionFsBinaryProvider}; +use github_copilot_sdk::{ + Client, DirEntry, DirEntryKind, FileInfo, SessionFsCapabilities, SessionFsConfig, + SessionFsConventions, SessionFsProvider, +}; + +struct BinaryFileProvider { + root: PathBuf, + read_paths: Mutex>, +} + +impl BinaryFileProvider { + fn path(&self, path: &str) -> PathBuf { + self.root.join(path.trim_start_matches('/')) + } +} + +#[async_trait] +impl SessionFsBinaryProvider for BinaryFileProvider { + async fn read_file_bytes(&self, path: &str) -> Result, FsError> { + self.read_paths.lock().unwrap().push(path.to_string()); + tokio::fs::read(self.path(path)) + .await + .map_err(FsError::from) + } + + async fn write_file_bytes( + &self, + path: &str, + content: &[u8], + _mode: Option, + ) -> Result<(), FsError> { + let target = self.path(path); + tokio::fs::create_dir_all(target.parent().unwrap()) + .await + .map_err(FsError::from)?; + tokio::fs::write(target, content) + .await + .map_err(FsError::from) + } +} + +#[async_trait] +impl SessionFsProvider for BinaryFileProvider { + fn binary(&self) -> Option<&dyn SessionFsBinaryProvider> { + Some(self) + } + + async fn read_file(&self, path: &str) -> Result { + tokio::fs::read_to_string(self.path(path)) + .await + .map_err(FsError::from) + } + + async fn write_file( + &self, + path: &str, + content: &str, + _mode: Option, + ) -> Result<(), FsError> { + let target = self.path(path); + tokio::fs::create_dir_all(target.parent().unwrap()) + .await + .map_err(FsError::from)?; + tokio::fs::write(target, content) + .await + .map_err(FsError::from) + } + + async fn append_file( + &self, + path: &str, + content: &str, + _mode: Option, + ) -> Result<(), FsError> { + use tokio::io::AsyncWriteExt; + + let target = self.path(path); + tokio::fs::create_dir_all(target.parent().unwrap()) + .await + .map_err(FsError::from)?; + let mut file = tokio::fs::OpenOptions::new() + .create(true) + .append(true) + .open(target) + .await + .map_err(FsError::from)?; + file.write_all(content.as_bytes()) + .await + .map_err(FsError::from) + } + + async fn exists(&self, path: &str) -> Result { + tokio::fs::try_exists(self.path(path)) + .await + .map_err(FsError::from) + } + + async fn stat(&self, path: &str) -> Result { + let metadata = tokio::fs::metadata(self.path(path)) + .await + .map_err(FsError::from)?; + Ok(FileInfo::new( + metadata.is_file(), + metadata.is_dir(), + metadata.len() as i64, + "1970-01-01T00:00:00Z", + "1970-01-01T00:00:00Z", + )) + } + + async fn mkdir(&self, path: &str, _recursive: bool, _mode: Option) -> Result<(), FsError> { + tokio::fs::create_dir_all(self.path(path)) + .await + .map_err(FsError::from) + } + + async fn readdir(&self, path: &str) -> Result, FsError> { + let mut dir = tokio::fs::read_dir(self.path(path)) + .await + .map_err(FsError::from)?; + let mut names = Vec::new(); + while let Some(entry) = dir.next_entry().await.map_err(FsError::from)? { + names.push(entry.file_name().to_string_lossy().into_owned()); + } + Ok(names) + } + + async fn readdir_with_types(&self, path: &str) -> Result, FsError> { + let mut dir = tokio::fs::read_dir(self.path(path)) + .await + .map_err(FsError::from)?; + let mut entries = Vec::new(); + while let Some(entry) = dir.next_entry().await.map_err(FsError::from)? { + let kind = if entry.file_type().await.map_err(FsError::from)?.is_dir() { + DirEntryKind::Directory + } else { + DirEntryKind::File + }; + entries.push(DirEntry::new(entry.file_name().to_string_lossy(), kind)); + } + Ok(entries) + } + + async fn rm(&self, path: &str, recursive: bool, force: bool) -> Result<(), FsError> { + let target = self.path(path); + let result = if recursive { + tokio::fs::remove_dir_all(target).await + } else { + tokio::fs::remove_file(target).await + }; + match result { + Err(err) if force && err.kind() == std::io::ErrorKind::NotFound => Ok(()), + result => result.map_err(FsError::from), + } + } + + async fn rename(&self, src: &str, dest: &str) -> Result<(), FsError> { + let target = self.path(dest); + tokio::fs::create_dir_all(target.parent().unwrap()) + .await + .map_err(FsError::from)?; + tokio::fs::rename(self.path(src), target) + .await + .map_err(FsError::from) + } +} + +#[tokio::test] +async fn should_route_file_operations_through_the_session_fs_provider() { + super::support::with_e2e_context( + "session_fs", + "should_route_file_operations_through_the_session_fs_provider", + |ctx| { + Box::pin(async move { + let provider = Arc::new(BinaryFileProvider { + root: ctx.work_dir().join("text-provider"), + read_paths: Mutex::new(Vec::new()), + }); + let session_state_path = if cfg!(windows) { + "/session-state".to_string() + } else { + ctx.work_dir() + .join("session-state") + .to_string_lossy() + .into_owned() + }; + let client = Client::start( + ctx.client_options().with_session_fs( + SessionFsConfig::new( + "/", + session_state_path.clone(), + SessionFsConventions::Posix, + ) + .with_capabilities(SessionFsCapabilities::new().with_binary(true)), + ), + ) + .await + .expect("start session client"); + let session = client + .create_session( + ctx.approve_all_session_config() + .with_session_fs_provider(provider.clone()), + ) + .await + .expect("create session"); + + let answer = session + .send_and_wait("What is 100 + 200?") + .await + .expect("send arithmetic request") + .expect("final assistant message"); + assert!( + answer.data["content"] + .as_str() + .is_some_and(|content| content.contains("300")), + "unexpected assistant response: {:?}", + answer.data + ); + session.disconnect().await.expect("disconnect session"); + let events_path = provider.path(&format!("{session_state_path}/events.jsonl")); + let events = tokio::fs::read_to_string(&events_path) + .await + .unwrap_or_else(|err| { + panic!("read provider events {}: {err}", events_path.display()) + }); + assert!( + events.contains("300"), + "provider events did not contain the response" + ); + client.stop().await.expect("stop client"); + }) + }, + ) + .await; +} + +#[tokio::test] +async fn should_view_an_image_that_exists_only_in_the_binary_session_fs_provider() { + super::support::with_e2e_context( + "session_fs", + "should_view_an_image_that_exists_only_in_the_binary_session_fs_provider", + |ctx| { + Box::pin(async move { + let image_path = "/sdk-provider-image.png"; + let image_bytes = base64::engine::general_purpose::STANDARD + .decode("iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8DwHwAFBQIAX8jx0gAAAABJRU5ErkJggg==") + .unwrap(); + let provider = Arc::new(BinaryFileProvider { + root: ctx.work_dir().join("binary-provider"), + read_paths: Mutex::new(Vec::new()), + }); + let session_state_path = if cfg!(windows) { + "/session-state".to_string() + } else { + ctx.work_dir() + .join("session-state") + .to_string_lossy() + .into_owned() + }; + let client = Client::start( + ctx.client_options().with_session_fs( + SessionFsConfig::new("/", session_state_path, SessionFsConventions::Posix) + .with_capabilities(SessionFsCapabilities::new().with_binary(true)), + ), + ) + .await + .expect("start binary session client"); + let session = client + .create_session( + ctx.approve_all_session_config() + .with_session_fs_provider(provider.clone()), + ) + .await + .expect("create binary session"); + let stored_path = provider.path(image_path); + tokio::fs::create_dir_all(stored_path.parent().unwrap()) + .await + .unwrap(); + tokio::fs::write(stored_path, &image_bytes).await.unwrap(); + assert!(!Path::new(image_path).exists()); + + let answer = session + .send_and_wait( + "Use the view tool to view /sdk-provider-image.png, then reply with exactly SDK_PROVIDER_IMAGE_DONE.", + ) + .await + .expect("send image view request") + .expect("final assistant message"); + assert!( + answer.data["content"] + .as_str() + .is_some_and(|content| content.contains("SDK_PROVIDER_IMAGE_DONE")), + "unexpected assistant response: {:?}", + answer.data + ); + assert!( + provider.read_paths.lock().unwrap().iter().any(|p| p == image_path), + "view did not request provider image bytes" + ); + let events = session.get_events().await.expect("get session events"); + assert!( + events.iter().any(|event| { + event.event_type == "session.binary_asset" + && event.data["mimeType"] == "image/png" + && event.data["data"] + == base64::engine::general_purpose::STANDARD.encode(&image_bytes) + }), + "missing image asset with exact provider bytes" + ); + session.disconnect().await.expect("disconnect session"); + client.stop().await.expect("stop client"); + }) + }, + ) + .await; +} diff --git a/rust/tests/e2e/subagent_hooks.rs b/rust/tests/e2e/subagent_hooks.rs index f106223e5a..56cad894cb 100644 --- a/rust/tests/e2e/subagent_hooks.rs +++ b/rust/tests/e2e/subagent_hooks.rs @@ -4,7 +4,7 @@ use std::sync::Arc; use async_trait::async_trait; use github_copilot_sdk::hooks::{ HookContext, PostToolUseInput, PostToolUseOutput, PreToolUseInput, PreToolUseOutput, - SessionHooks, + SessionHooks, SubagentStartInput, SubagentStartOutput, SubagentStopInput, SubagentStopOutput, }; use github_copilot_sdk::session_events::SessionEventType; use github_copilot_sdk::{ @@ -16,14 +16,17 @@ use tokio::sync::watch; use super::support::{assistant_message_content, wait_for_event, with_e2e_context}; +const CHILD_CONTEXT: &str = "Subagent start hook verified: read the requested file."; +const STOP_RESPONSE_PREFIX: &str = "Subagent stop hook verified: "; + #[tokio::test] -async fn should_invoke_pretooluse_and_posttooluse_hooks_for_sub_agent_tool_calls() { +async fn should_apply_subagent_lifecycle_hook_outputs() { if super::support::skip_inprocess("LLM inference providers are process-global in-process") { return; } with_e2e_context( "subagent_hooks", - "should_invoke_pretooluse_and_posttooluse_hooks_for_sub_agent_tool_calls", + "should_apply_subagent_lifecycle_hook_outputs", |ctx| { Box::pin(async move { ctx.set_default_copilot_user(); @@ -34,6 +37,8 @@ async fn should_invoke_pretooluse_and_posttooluse_hooks_for_sub_agent_tool_calls .expect("write test file"); let hook_log = Arc::new(Mutex::new(Vec::::new())); + let subagent_starts = Arc::new(Mutex::new(Vec::new())); + let subagent_stops = Arc::new(Mutex::new(Vec::new())); let request_log = Arc::new(RecordingRequestHandler::default()); let waiting_text = "I've launched an explore agent to read subagent-test.txt. Waiting for it to complete..."; let final_text = "The explore agent successfully read the file. The contents of **subagent-test.txt** are:\n\n```\nHello from subagent test!\n```"; @@ -51,6 +56,8 @@ async fn should_invoke_pretooluse_and_posttooluse_hooks_for_sub_agent_tool_calls RecordingHooks { log: Arc::clone(&hook_log), parent_reply_observed, + subagent_starts: Arc::clone(&subagent_starts), + subagent_stops: Arc::clone(&subagent_stops), }, ))) .await @@ -133,7 +140,76 @@ async fn should_invoke_pretooluse_and_posttooluse_hooks_for_sub_agent_tool_calls task_pre.unwrap().session_id, "Sub-agent tool hooks should have a different sessionId than parent tool hooks" ); - assert_subagent_request_metadata(&request_log.inference_records()); + let requests = request_log.inference_records(); + assert_subagent_request_metadata(&requests); + let context_and_task = format!("{CHILD_CONTEXT}\n\nRead the file \"subagent-test.txt\""); + let prompt_has_context = |prompt: &str| prompt.contains(context_and_task.as_str()); + assert!( + requests + .iter() + .filter(|request| request.parent_agent_id.is_some()) + .any(|request| { + let body: serde_json::Value = serde_json::from_str(&request.body) + .expect("child inference request body"); + body["messages"].as_array().is_some_and(|messages| { + messages.iter().any(|message| { + message["role"] == "user" + && (message["content"] + .as_str() + .is_some_and(prompt_has_context) + || message["content"].as_array().is_some_and(|parts| { + parts.iter().any(|part| { + part["type"] == "text" + && part["text"] + .as_str() + .is_some_and(prompt_has_context) + }) + })) + }) + }) + }), + "start hook context should be prepended to the child inference prompt" + ); + assert!( + requests.iter().any(|request| { + request.parent_agent_id.is_none() + && request.body.contains(STOP_RESPONSE_PREFIX) + }), + "rewritten stop response should reach a parent inference request" + ); + + { + let starts = subagent_starts.lock(); + assert_eq!(starts.len(), 1, "one subagentStart per launched child"); + let start = &starts[0]; + assert_eq!(start.session_id, session.id().as_str()); + assert!(start.timestamp > 0.0); + assert_eq!( + start + .working_directory + .canonicalize() + .expect("canonical hook working directory"), + ctx.work_dir().canonicalize().expect("canonical test directory") + ); + assert_eq!(start.agent_name, "explore"); + assert_eq!(start.agent_display_name, None); + assert_eq!(start.agent_description, None); + + let stops = subagent_stops.lock(); + assert_eq!(stops.len(), 1, "one subagentStop per completed child"); + let stop = &stops[0]; + assert_eq!(stop.session_id, start.session_id); + assert!(stop.timestamp >= start.timestamp); + assert_eq!(stop.working_directory, start.working_directory); + assert_eq!(stop.transcript_path, start.transcript_path); + assert_eq!(stop.agent_name, start.agent_name); + assert_eq!(stop.agent_type, "explore"); + assert!(stop.agent_id.as_ref().is_some_and(|id| !id.is_empty())); + assert_eq!(stop.agent_display_name, start.agent_display_name); + assert_eq!(stop.agent_description, start.agent_description); + assert_eq!(stop.stop_reason, "end_turn"); + assert!(stop.response.contains("Hello from subagent test!")); + } session.disconnect().await.expect("disconnect session"); client.stop().await.expect("stop client"); @@ -156,6 +232,7 @@ struct RequestEntry { agent_id: Option, parent_agent_id: Option, interaction_type: Option, + body: String, } #[derive(Default)] @@ -186,6 +263,13 @@ impl CopilotRequestHandler for RecordingRequestHandler { agent_id: ctx.agent_id.clone(), parent_agent_id: ctx.parent_agent_id.clone(), interaction_type: ctx.interaction_type.clone(), + body: if is_inference_url(&request.url) { + std::str::from_utf8(&request.body) + .expect("inference request body is UTF-8") + .to_owned() + } else { + String::new() + }, }); forward_http(request).await } @@ -237,6 +321,8 @@ fn assert_subagent_request_metadata(records: &[RequestEntry]) { struct RecordingHooks { log: Arc>>, parent_reply_observed: watch::Receiver, + subagent_starts: Arc>>, + subagent_stops: Arc>>, } fn is_subagent_view(tool_name: &str, session_id: &str, log: &[HookEntry]) -> bool { @@ -262,6 +348,30 @@ fn only_child_view_waits_for_parent_reply() { #[async_trait] impl SessionHooks for RecordingHooks { + async fn on_subagent_start( + &self, + input: SubagentStartInput, + _ctx: HookContext, + ) -> Option { + self.subagent_starts.lock().push(input); + Some(SubagentStartOutput { + additional_context: Some(CHILD_CONTEXT.to_string()), + }) + } + + async fn on_subagent_stop( + &self, + input: SubagentStopInput, + _ctx: HookContext, + ) -> Option { + let response = format!("{STOP_RESPONSE_PREFIX}{}", input.response); + self.subagent_stops.lock().push(input); + Some(SubagentStopOutput { + modified_response: Some(response), + ..SubagentStopOutput::default() + }) + } + async fn on_pre_tool_use( &self, input: PreToolUseInput, diff --git a/rust/tests/e2e/tools.rs b/rust/tests/e2e/tools.rs index 586a31d3a1..3a03a7df94 100644 --- a/rust/tests/e2e/tools.rs +++ b/rust/tests/e2e/tools.rs @@ -1,3 +1,5 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. + use std::sync::Arc; use github_copilot_sdk::handler::{ApproveAllHandler, PermissionHandler, PermissionResult}; @@ -11,6 +13,54 @@ use tokio::sync::{Mutex, mpsc}; use super::support::{assistant_message_content, recv_with_timeout}; +#[cfg(feature = "derive")] +#[tokio::test] +async fn string_schema_apply_patch_override_binds_patch_input() { + super::support::with_shared_e2e_context( + &E2E, + "tools", + "string_schema_apply_patch_override_binds_patch_input", + |ctx| { + Box::pin(async move { + ctx.set_default_copilot_user(); + let client = ctx.start_client().await; + let (tx, mut rx) = mpsc::unbounded_channel(); + let tool = github_copilot_sdk::tool::define_tool::( + "apply_patch", + "Apply a patch", + move |_invocation, input| { + let tx = tx.clone(); + async move { + tx.send(input).expect("capture patch"); + Ok(ToolResult::Text("HOST_PATCH_HANDLED".to_owned())) + } + }, + ) + .with_overrides_built_in_tool(true); + let session = client + .create_session(ctx.approve_all_session_config().with_tools(vec![tool])) + .await + .expect("create session"); + let response = session + .send_and_wait("Use apply_patch to apply the supplied patch.") + .await + .expect("send") + .expect("assistant message"); + + assert_eq!( + recv_with_timeout(&mut rx, "string-schema patch").await, + "*** Begin Patch\n*** Add File: override-marker.txt\n+from-native\n*** End Patch" + ); + assert_eq!(assistant_message_content(&response), "Host override completed."); + assert!(!ctx.work_dir().join("override-marker.txt").exists()); + session.disconnect().await.expect("disconnect session"); + client.stop().await.expect("stop client"); + }) + }, + ) + .await; +} + #[tokio::test] async fn invokes_built_in_tools() { super::support::with_shared_e2e_context(&E2E, "tools", "invokes_built_in_tools", |ctx| { @@ -880,4 +930,7 @@ impl ToolHandler for DbQueryTool { )) } } -static E2E: super::support::SharedE2eGroup = super::support::SharedE2eGroup::standard("tools", 11); +static E2E: super::support::SharedE2eGroup = super::support::SharedE2eGroup::standard( + "tools", + if cfg!(feature = "derive") { 12 } else { 11 }, +); diff --git a/rust/tests/session_events_test.rs b/rust/tests/session_events_test.rs index 401fa0a09c..9ccb5c833c 100644 --- a/rust/tests/session_events_test.rs +++ b/rust/tests/session_events_test.rs @@ -3,11 +3,28 @@ #![allow(clippy::unwrap_used)] use github_copilot_sdk::session_events::{ - IndexedSearchData, PermissionApprovalEvaluation, PermissionApprovalEvaluationEvaluationStage, - PermissionApprovalEvaluationJudgeStatus, PermissionApprovalEvaluationReasonCode, - SandboxDecisionData, SessionEventData, TypedSessionEvent, UserMessageData, + HumanResponseRecordedResponse, IndexedSearchData, PermissionApprovalEvaluation, + PermissionApprovalEvaluationEvaluationStage, PermissionApprovalEvaluationJudgeStatus, + PermissionApprovalEvaluationReasonCode, SandboxDecisionData, SessionEventData, + ToolExecutionCompleteFileEdit, ToolExecutionCompleteFileEditKind, TypedSessionEvent, + UserMessageData, }; +#[test] +fn future_file_edit_kind_retains_its_wire_value() { + let wire = serde_json::json!({ "path": "/future.txt", "kind": "append" }); + let edit: ToolExecutionCompleteFileEdit = serde_json::from_value(wire.clone()).unwrap(); + assert_eq!( + edit.kind, + ToolExecutionCompleteFileEditKind::Unknown("append".to_owned()) + ); + assert_eq!(serde_json::to_value(edit).unwrap(), wire); + assert_eq!( + serde_json::to_value(ToolExecutionCompleteFileEditKind::Create).unwrap(), + "create" + ); +} + fn event_envelope(event_type: &str, data: serde_json::Value) -> serde_json::Value { serde_json::json!({ "id": "10000000-0000-4000-8000-000000000001", @@ -217,6 +234,90 @@ fn ordinary_and_empty_payloads_round_trip_full_event_envelopes() { } } +#[test] +fn human_response_variants_round_trip_as_typed_event_data() { + let payloads = [ + serde_json::json!({ + "requestId": "ask-user-request", + "toolCallId": "ask-user-tool", + "actor": "human_response", + "response": { + "responseKind": "ask_user", + "message": "Which repository?", + "requestedSchema": { + "type": "object", + "properties": { + "answer": { + "type": "string" + } + } + }, + "content": { + "answer": "github/st-mcp-poc" + } + } + }), + serde_json::json!({ + "requestId": "question-request", + "toolCallId": "question-tool", + "actor": "human_response", + "response": { + "responseKind": "user_input", + "question": "Which file?", + "choices": ["alpha.txt", "beta.txt"], + "allowFreeform": true, + "answer": "alpha.txt", + "wasFreeform": false + } + }), + serde_json::json!({ + "requestId": "plan-request", + "toolCallId": "plan-tool", + "actor": "human_response", + "response": { + "responseKind": "exit_plan_mode", + "summary": "Edit alpha.txt", + "planContent": "Edit alpha.txt and run its test.", + "actions": ["autopilot", "interactive", "exit_only"], + "recommendedAction": "interactive", + "approved": true, + "selectedAction": "interactive", + "autoApproveEdits": false + } + }), + ]; + + for wire in payloads.map(|payload| event_envelope("human_response.recorded", payload)) { + let event: TypedSessionEvent = serde_json::from_value(wire.clone()).unwrap(); + let SessionEventData::HumanResponseRecorded(data) = &event.payload else { + panic!("expected human-response event"); + }; + let response_kind = match &data.response { + HumanResponseRecordedResponse::AskUser(response) => { + assert_eq!( + response.content.get("answer"), + Some(&serde_json::json!("github/st-mcp-poc")) + ); + "ask_user" + } + HumanResponseRecordedResponse::UserInput(response) => { + assert_eq!(response.answer, "alpha.txt"); + "user_input" + } + HumanResponseRecordedResponse::ExitPlanMode(response) => { + assert!(response.approved); + "exit_plan_mode" + } + }; + + assert_eq!( + response_kind, + wire["data"]["response"]["responseKind"].as_str().unwrap() + ); + assert_eq!(serde_json::to_value(event).unwrap(), wire); + } +} + #[test] fn approval_evaluation_preserves_protocol_unknown_values() { let wire = serde_json::json!({ diff --git a/rust/tests/session_test.rs b/rust/tests/session_test.rs index e38b0c427d..23a254d9e9 100644 --- a/rust/tests/session_test.rs +++ b/rust/tests/session_test.rs @@ -6902,6 +6902,90 @@ async fn hooks_invoke_returns_empty_for_unregistered_hook() { assert_eq!(response["result"]["output"], serde_json::json!({})); } +#[tokio::test] +async fn hooks_invoke_subagent_lifecycle_does_not_warn_as_unknown() { + use github_copilot_sdk::hooks::SessionHooks; + + struct EmptyHooks; + #[async_trait] + impl SessionHooks for EmptyHooks {} + + let (capture, _guard) = capture_traces(); + let (_session, mut server) = create_session_pair_with_hooks(Arc::new(EmptyHooks)).await; + + for (request_id, hook_type, input) in [ + ( + 302, + "subagentStart", + serde_json::json!({ + "sessionId": server.session_id, + "timestamp": 1234567890, + "cwd": "/tmp", + "transcriptPath": "/tmp/transcript.jsonl", + "agentName": "task" + }), + ), + ( + 303, + "subagentStop", + serde_json::json!({ + "sessionId": server.session_id, + "timestamp": 1234567890, + "cwd": "/tmp", + "transcriptPath": "/tmp/transcript.jsonl", + "agentName": "task", + "agentType": "task", + "stopReason": "end_turn", + "response": "done" + }), + ), + ] { + server + .send_request( + request_id, + "hooks.invoke", + serde_json::json!({ + "sessionId": server.session_id, + "hookType": hook_type, + "input": input + }), + ) + .await; + let response = timeout(TIMEOUT, server.read_response()).await.unwrap(); + assert_eq!(response["id"], request_id); + assert_eq!(response["result"], serde_json::json!({ "output": {} })); + } + + server + .send_request( + 304, + "hooks.invoke", + serde_json::json!({ + "sessionId": server.session_id, + "hookType": "unrecognizedHook", + "input": {} + }), + ) + .await; + let response = timeout(TIMEOUT, server.read_response()).await.unwrap(); + assert_eq!(response["result"], serde_json::json!({ "output": {} })); + + let warnings: Vec<_> = capture + .events + .lock() + .unwrap() + .iter() + .filter(|event| event.message_contains("unknown hook type")) + .cloned() + .collect(); + assert_eq!(warnings.len(), 1, "unknown hook warnings: {warnings:?}"); + assert!( + warnings[0].field_is("hook_type", "unrecognizedHook"), + "unexpected hook warning: {:?}", + warnings[0] + ); +} + async fn create_session_pair_with_system_message_transforms( transforms: Arc, ) -> (github_copilot_sdk::session::Session, FakeServer) { @@ -7747,11 +7831,58 @@ async fn command_execute_handler_error_propagates_to_ack() { // SessionFsProvider tests -------------------------------------------------- use github_copilot_sdk::session_fs::{ - DirEntry, DirEntryKind, FileInfo, FsError, FsErrorKind, SessionFsConventions, - SessionFsProvider, SessionFsSqliteProvider, SessionFsSqliteQueryResult, + DirEntry, DirEntryKind, FileInfo, FsError, FsErrorKind, SessionFsCapabilities, SessionFsConfig, + SessionFsConventions, SessionFsProvider, SessionFsSqliteProvider, SessionFsSqliteQueryResult, SessionFsSqliteQueryType, SessionFsSqliteTransactionError, SessionFsSqliteTransactionStatement, }; +#[tokio::test] +async fn binary_capability_rejects_text_only_provider_on_create_and_resume() { + for resume in [false, true] { + let (client_write, mut server_read) = duplex(8192); + let (server_write, client_read) = duplex(8192); + let config = SessionFsConfig::new("/workspace", "/sessions", SessionFsConventions::Posix) + .with_capabilities(SessionFsCapabilities::new().with_binary(true)); + let client = Client::from_streams_with_session_fs_config( + client_read, + client_write, + std::env::temp_dir(), + config, + ) + .unwrap(); + let provider = Arc::new(RecordingFsProvider::new()); + let result = timeout(TIMEOUT, async { + if resume { + client + .resume_session( + github_copilot_sdk::ResumeSessionConfig::new(SessionId::new( + "text-only-provider", + )) + .with_session_fs_provider(provider), + ) + .await + } else { + client + .create_session(SessionConfig::default().with_session_fs_provider(provider)) + .await + } + }) + .await + .expect("provider validation must complete before an RPC"); + let error = result.err().expect("text-only provider must be rejected"); + assert!(matches!(error.kind(), ErrorKind::InvalidConfig)); + assert!(error.to_string().contains("binary"), "{error}"); + assert_eq!(client.registered_session_count_for_test(), 0); + assert!( + timeout(Duration::from_millis(50), read_framed(&mut server_read)) + .await + .is_err(), + "invalid provider must not send a session RPC", + ); + drop(server_write); + } +} + struct RecordingFsProvider { files: parking_lot::Mutex>, blocked_stat: Option<(Arc, Arc)>, diff --git a/scripts/build-prerequisites.mjs b/scripts/build-prerequisites.mjs index 63624b33e9..0b0c1d19fc 100644 --- a/scripts/build-prerequisites.mjs +++ b/scripts/build-prerequisites.mjs @@ -6,6 +6,7 @@ import { spawnSync } from "node:child_process"; import fs from "node:fs"; import os from "node:os"; import path from "node:path"; +import { installNpmDependencies } from "./install-dependencies.mjs"; export const SCHEMA_FILES = ["api.schema.json", "session-events.schema.json"]; export const GENERATED_ROOTS = { @@ -104,10 +105,10 @@ export function bazelActionEnvironmentArgument() { return "--action_env=PATH"; } -function installCodegenDependencies(languages, sdkRoot) { - runNpm(["ci", "--ignore-scripts"], path.join(sdkRoot, "scripts/codegen")); +export function installCodegenDependencies(languages, sdkRoot) { + installNpmDependencies(path.join(sdkRoot, "scripts/codegen"), runNpm); if (languages.includes("java")) { - runNpm(["ci", "--ignore-scripts"], path.join(sdkRoot, "java/scripts/codegen")); + installNpmDependencies(path.join(sdkRoot, "java/scripts/codegen"), runNpm); } } @@ -155,7 +156,11 @@ export function syncGeneratedArchive({ try { const extraction = archiveExtractionInvocation(archivePath, stagingDirectory); run(tarCommand(), extraction.args, extraction.cwd); - for (const file of listFiles(stagingDirectory)) { + const files = listFiles(stagingDirectory); + if (files.length === 0) { + throw new Error(`No ${language} generated outputs in ${archivePath}`); + } + for (const file of files) { if (!matchesGeneratedPath(file, generatedRoots)) { throw new Error(`Undeclared ${language} generated output: ${file}`); } diff --git a/scripts/build-prerequisites.test.mjs b/scripts/build-prerequisites.test.mjs index 2114c0e207..feae19fedd 100644 --- a/scripts/build-prerequisites.test.mjs +++ b/scripts/build-prerequisites.test.mjs @@ -3,8 +3,10 @@ *--------------------------------------------------------------------------------------------*/ import assert from "node:assert/strict"; +import childProcess from "node:child_process"; import { spawnSync } from "node:child_process"; import fs from "node:fs"; +import { syncBuiltinESMExports } from "node:module"; import os from "node:os"; import path from "node:path"; import test from "node:test"; @@ -13,6 +15,7 @@ import { archiveExtractionInvocation, bazelActionEnvironmentArgument, bazelInvocationEnvironment, + installCodegenDependencies, npmInvocation, parseBazelInfoPath, syncGeneratedArchive, @@ -72,6 +75,48 @@ test("runs npm through the Windows command interpreter", () => { }); }); +test("installs shared codegen dependencies once and Java dependencies only when selected", (t) => { + const sdkRoot = fs.mkdtempSync(path.join(import.meta.dirname, ".codegen-dependencies-")); + t.after(() => fs.rmSync(sdkRoot, { recursive: true, force: true })); + for (const directory of ["scripts/codegen", "java/scripts/codegen"]) { + writeFile(path.join(sdkRoot, directory, "package.json"), "{}"); + writeFile(path.join(sdkRoot, directory, "package-lock.json"), "{}"); + } + const calls = []; + const spawn = t.mock.method(childProcess, "spawnSync", (command, args, { cwd }) => { + calls.push({ command, args, cwd }); + return { status: 0 }; + }); + syncBuiltinESMExports(); + t.after(() => { + spawn.mock.restore(); + syncBuiltinESMExports(); + }); + const invocation = npmInvocation(); + const shared = path.join(sdkRoot, "scripts/codegen"); + const java = path.join(sdkRoot, "java/scripts/codegen"); + + installCodegenDependencies(["nodejs", "rust"], sdkRoot); + installCodegenDependencies(["nodejs"], sdkRoot); + assert.deepEqual(calls, [ + { + command: invocation.command, + args: [...invocation.args, "ci", "--ignore-scripts", "--include=dev"], + cwd: shared, + }, + ]); + assert.equal(fs.existsSync(path.join(java, "node_modules")), false); + + installCodegenDependencies(["java"], sdkRoot); + installCodegenDependencies(["java"], sdkRoot); + assert.equal(calls.length, 2); + assert.deepEqual(calls[1], { + command: invocation.command, + args: [...invocation.args, "ci", "--ignore-scripts", "--include=dev"], + cwd: java, + }); +}); + test("syncs generated projections once without rewriting unchanged files", (t) => { const root = createGitFixture(t); const sdkRoot = path.join(root, "src/sdk"); @@ -134,6 +179,32 @@ test("refuses to overwrite modified generated projections", (t) => { assert.equal(fs.readFileSync(path.join(sdkRoot, generatedPath), "utf8"), "local edit\n"); }); +test("rejects empty generator archives without deleting existing projections", (t) => { + const root = createGitFixture(t); + const sdkRoot = path.join(root, "src/sdk"); + const generatedPath = "nodejs/src/generated/rpc.ts"; + writeFile(path.join(sdkRoot, generatedPath), "tracked\n"); + run("git", ["add", "."], root); + run("git", ["commit", "-m", "fixture"], root); + const stagedRoot = path.join(root, "staged"); + fs.mkdirSync(stagedRoot); + const archivePath = path.join(root, "projection.tar"); + run(tarCommand(), ["-cf", archivePath, "-C", stagedRoot, "."], root); + + assert.throws( + () => + syncGeneratedArchive({ + archivePath, + generatedRoots: ["nodejs/src/generated"], + language: "nodejs", + runtimeRoot: root, + sdkRoot, + }), + /No nodejs generated outputs/, + ); + assert.equal(fs.readFileSync(path.join(sdkRoot, generatedPath), "utf8"), "tracked\n"); +}); + test("replaces prior generated projections while preserving manual edits", (t) => { const root = createGitFixture(t); const sdkRoot = path.join(root, "src/sdk"); diff --git a/scripts/ci/check-generated.mjs b/scripts/ci/check-generated.mjs index 2298ea57be..3eb90b508a 100644 --- a/scripts/ci/check-generated.mjs +++ b/scripts/ci/check-generated.mjs @@ -23,7 +23,7 @@ export const PROTOCOL_FILES = [ const SDK_INPUTS = [ /^scripts\/codegen\//, /^java\/scripts\/codegen\//, - /^scripts\/(?:build-prerequisites|run-tasks|runtime-layout)\.mjs$/, + /^scripts\/(?:build-prerequisites|install-dependencies|run-tasks|runtime-layout)\.mjs$/, /^scripts\/ci\/check-generated\.mjs$/, /^(?:BUILD\.bazel|package\.json|\.editorconfig)$/, /^nodejs\/(?:scripts\/releaseArtifacts\.ts|src\/cliVersion\.ts|package(?:-lock)?\.json)$/, diff --git a/scripts/ci/check-generated.test.mjs b/scripts/ci/check-generated.test.mjs index 38883ec7dd..23c95ced04 100644 --- a/scripts/ci/check-generated.test.mjs +++ b/scripts/ci/check-generated.test.mjs @@ -150,6 +150,7 @@ test("skips projections for internal-only and SDK documentation changes", (t) => for (const file of [ "src/sdk/scripts/codegen/go.ts", "src/sdk/scripts/codegen/package-lock.json", + "src/sdk/scripts/install-dependencies.mjs", "src/sdk/java/scripts/codegen/java.ts", "src/sdk/rust/.rustfmt.nightly.toml", "src/sdk/go/types.go", diff --git a/scripts/ci/install-dependencies.test.mjs b/scripts/ci/install-dependencies.test.mjs new file mode 100644 index 0000000000..0799228417 --- /dev/null +++ b/scripts/ci/install-dependencies.test.mjs @@ -0,0 +1,185 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +import assert from "node:assert/strict"; +import { spawnSync } from "node:child_process"; +import { createHash } from "node:crypto"; +import fs from "node:fs"; +import path from "node:path"; +import test from "node:test"; + +import { installNpmDependencies } from "../install-dependencies.mjs"; +import { npmInvocation } from "../build-prerequisites.mjs"; + +test("installs once and fingerprints the install arguments and manifests", (t) => { + const { directory, stampFile } = fixture(t); + const calls = []; + const runNpm = (args, cwd) => calls.push({ args, cwd }); + + assert.equal(installNpmDependencies(directory, runNpm), true); + assert.deepEqual(calls, [{ args: ["ci", "--ignore-scripts", "--include=dev"], cwd: directory }]); + const expectedFingerprint = createHash("sha256") + .update(JSON.stringify(["ci", "--ignore-scripts", "--include=dev"])) + .update(fs.readFileSync(path.join(directory, "package-lock.json"))) + .update(fs.readFileSync(path.join(directory, "package.json"))) + .digest("hex"); + assert.equal(fs.readFileSync(stampFile, "utf8"), `${expectedFingerprint}\n`); + + assert.equal(installNpmDependencies(directory, runNpm), false); + assert.equal(calls.length, 1); +}); + +for (const file of ["package.json", "package-lock.json"]) { + test(`reinstalls when ${file} changes`, (t) => { + const { directory, stampFile } = fixture(t); + const calls = []; + const runNpm = (args) => calls.push(args); + installNpmDependencies(directory, runNpm); + const previousStamp = fs.readFileSync(stampFile, "utf8"); + fs.appendFileSync(path.join(directory, file), "\n"); + + assert.equal(installNpmDependencies(directory, runNpm), true); + assert.equal(calls.length, 2); + assert.notEqual(fs.readFileSync(stampFile, "utf8"), previousStamp); + }); +} + +for (const missing of ["stamp", "node_modules"]) { + test(`reinstalls when ${missing} is missing`, (t) => { + const { directory, stampFile } = fixture(t); + installNpmDependencies(directory, () => {}); + fs.rmSync(missing === "stamp" ? stampFile : path.dirname(stampFile), { recursive: true }); + + const calls = []; + assert.equal( + installNpmDependencies(directory, (args) => calls.push(args)), + true, + ); + assert.deepEqual(calls, [["ci", "--ignore-scripts", "--include=dev"]]); + assert.equal(fs.existsSync(stampFile), true); + }); +} + +test("removes the old stamp before installing and leaves it absent after failure", (t) => { + const { directory, stampFile } = fixture(t); + installNpmDependencies(directory, () => {}); + fs.appendFileSync(path.join(directory, "package-lock.json"), "\n"); + const failure = new Error("npm ci failed"); + let attempts = 0; + + assert.throws( + () => + installNpmDependencies(directory, () => { + attempts++; + assert.equal(fs.existsSync(stampFile), false); + throw failure; + }), + (error) => error === failure, + ); + assert.equal(attempts, 1); + assert.equal(fs.existsSync(stampFile), false); + assert.equal( + installNpmDependencies(directory, () => {}), + true, + ); + assert.equal(fs.existsSync(stampFile), true); +}); + +test("surfaces unexpected stamp read errors without attempting an install", (t) => { + const { directory, stampFile } = fixture(t); + fs.mkdirSync(stampFile, { recursive: true }); + let installed = false; + + assert.throws( + () => + installNpmDependencies(directory, () => { + installed = true; + }), + { code: "EISDIR" }, + ); + assert.equal(installed, false); +}); + +for (const environment of [{ NODE_ENV: "production" }, { npm_config_omit: "dev" }]) { + test(`installs build tools despite ${JSON.stringify(environment)} and reuses them on a normal retry`, (t) => { + const { directory, runNpm, buildTool } = npmFixture(t); + + assert.equal( + installNpmDependencies(directory, (args, cwd) => runNpm(args, cwd, environment)), + true, + ); + assert.equal(fs.existsSync(buildTool), true); + assert.equal(installNpmDependencies(directory, runNpm), false); + assert.equal(fs.existsSync(buildTool), true); + }); +} + +test("repairs a production-only install recorded by the old manifest-only stamp", (t) => { + const { directory, stampFile, runNpm, buildTool } = npmFixture(t); + runNpm(["ci", "--ignore-scripts"], directory, { NODE_ENV: "production" }); + assert.equal(fs.existsSync(buildTool), false); + const legacyFingerprint = createHash("sha256") + .update(fs.readFileSync(path.join(directory, "package-lock.json"))) + .update(fs.readFileSync(path.join(directory, "package.json"))) + .digest("hex"); + fs.mkdirSync(path.dirname(stampFile), { recursive: true }); + fs.writeFileSync(stampFile, `${legacyFingerprint}\n`); + + assert.equal(installNpmDependencies(directory, runNpm), true); + assert.equal(fs.existsSync(buildTool), true); + assert.equal(installNpmDependencies(directory, runNpm), false); +}); + +function npmFixture(t) { + const fixturePaths = fixture(t); + const { directory } = fixturePaths; + fs.mkdirSync(path.join(directory, "build-tool")); + fs.writeFileSync( + path.join(directory, "build-tool/package.json"), + JSON.stringify({ name: "fixture-build-tool", version: "1.0.0" }), + ); + fs.writeFileSync( + path.join(directory, "package.json"), + JSON.stringify({ name: "fixture", devDependencies: { "fixture-build-tool": "file:./build-tool" } }), + ); + const environment = Object.fromEntries( + Object.entries(process.env).filter(([name]) => name !== "NODE_ENV" && !/^npm_config_/i.test(name)), + ); + const userConfig = path.join(directory, "user.npmrc"); + const globalConfig = path.join(directory, "global.npmrc"); + fs.writeFileSync(userConfig, ""); + fs.writeFileSync(globalConfig, ""); + const invocation = npmInvocation(); + const runNpm = (args, cwd, overrides = {}) => { + const result = spawnSync( + invocation.command, + [ + ...invocation.args, + ...args, + "--offline", + "--no-audit", + "--no-fund", + `--userconfig=${userConfig}`, + `--globalconfig=${globalConfig}`, + `--cache=${path.join(directory, "cache")}`, + ], + { cwd, env: { ...environment, ...overrides }, encoding: "utf8", timeout: 30_000 }, + ); + assert.equal(result.status, 0, result.error?.message ?? `${result.stdout}\n${result.stderr}`); + }; + runNpm(["install", "--package-lock-only", "--ignore-scripts"], directory); + return { + ...fixturePaths, + runNpm, + buildTool: path.join(directory, "node_modules/fixture-build-tool/package.json"), + }; +} + +function fixture(t) { + const directory = fs.mkdtempSync(path.join(import.meta.dirname, ".install-dependencies-")); + t.after(() => fs.rmSync(directory, { recursive: true, force: true })); + fs.writeFileSync(path.join(directory, "package.json"), '{"name":"fixture"}\n'); + fs.writeFileSync(path.join(directory, "package-lock.json"), '{"lockfileVersion":3}\n'); + return { directory, stampFile: path.join(directory, "node_modules/.copilot-sdk-install-stamp") }; +} diff --git a/scripts/ci/run-dotnet-tests.sh b/scripts/ci/run-dotnet-tests.sh index a178357477..ea0312c4a6 100755 --- a/scripts/ci/run-dotnet-tests.sh +++ b/scripts/ci/run-dotnet-tests.sh @@ -11,6 +11,7 @@ Usage: run-dotnet-tests.sh [--help] Runs the full .NET SDK test project. Environment variables: DOTNET_TEST_FILTER Optional dotnet test filter (e.g. for a backend or transport). DOTNET_TEST_RUNTIME Optional runtime identifier passed to dotnet test. + DOTNET_TEST_RESULTS_DIRECTORY Results directory (default: TestResults). EOF } @@ -34,7 +35,7 @@ args=( --blame-hang-timeout 10m --blame-hang-dump-type none --logger "trx;LogFilePrefix=test-results" - --results-directory TestResults + --results-directory "${DOTNET_TEST_RESULTS_DIRECTORY:-TestResults}" -p:RunAnalyzers=false ) filter="${DOTNET_TEST_FILTER:-}" diff --git a/scripts/ci/run-dotnet-tests.test.mjs b/scripts/ci/run-dotnet-tests.test.mjs index 6a1be129e1..582fcd6b2c 100644 --- a/scripts/ci/run-dotnet-tests.test.mjs +++ b/scripts/ci/run-dotnet-tests.test.mjs @@ -19,7 +19,7 @@ function fromBashPath(value) { return value; } -await test("runs all .NET tests and forwards only explicit filter and runtime", (t) => { +await test("runs all .NET tests with optional filter, runtime, and results directory", (t) => { const root = fs.mkdtempSync(path.join(os.tmpdir(), "sdk-dotnet-test-")); t.after(() => fs.rmSync(root, { recursive: true, force: true })); const argumentsPath = path.join(root, "arguments"); @@ -31,9 +31,9 @@ await test("runs all .NET tests and forwards only explicit filter and runtime", ); fs.chmodSync(fakeDotnet, 0o755); - for (const [filter, runtime] of [ - ["", ""], - ["E2EBackend!=CapiOnly", "win-x64"], + for (const [filter, runtime, resultsDirectory] of [ + ["", "", ""], + ["E2EBackend!=CapiOnly", "win-x64", "TestResults/inprocess"], ]) { const result = spawnSync("bash", [script], { encoding: "utf8", @@ -42,6 +42,7 @@ await test("runs all .NET tests and forwards only explicit filter and runtime", ARGUMENTS_PATH: argumentsPath, DOTNET_TEST_FILTER: filter, DOTNET_TEST_RUNTIME: runtime, + DOTNET_TEST_RESULTS_DIRECTORY: resultsDirectory, PATH: `${root}${path.delimiter}${process.env.PATH ?? ""}`, WORKING_DIRECTORY_PATH: workingDirectoryPath, }, @@ -54,6 +55,10 @@ await test("runs all .NET tests and forwards only explicit filter and runtime", ); const arguments_ = fs.readFileSync(argumentsPath, "utf8").trim().split("\n"); assert.deepEqual(arguments_.slice(0, 3), ["test", "test/GitHub.Copilot.SDK.Test.csproj", "--no-build"]); + assert.equal( + arguments_[arguments_.indexOf("--results-directory") + 1], + resultsDirectory || "TestResults", + ); assert.equal(arguments_.includes("--filter"), filter !== ""); if (filter) { assert.equal(arguments_[arguments_.indexOf("--filter") + 1], filter); @@ -70,6 +75,7 @@ await test("documents its environment and rejects positional arguments", () => { assert.equal(help.status, 0, help.stderr); assert.match(help.stdout, /DOTNET_TEST_FILTER/); assert.match(help.stdout, /DOTNET_TEST_RUNTIME/); + assert.match(help.stdout, /DOTNET_TEST_RESULTS_DIRECTORY/); const invalid = spawnSync("bash", [script, "unexpected"], { encoding: "utf8" }); assert.equal(invalid.status, 2); diff --git a/scripts/ci/run-tasks.test.mjs b/scripts/ci/run-tasks.test.mjs index e4c5bfd7c9..99c456e3c5 100644 --- a/scripts/ci/run-tasks.test.mjs +++ b/scripts/ci/run-tasks.test.mjs @@ -3,7 +3,12 @@ *--------------------------------------------------------------------------------------------*/ import assert from "node:assert/strict"; +import childProcess from "node:child_process"; +import fs from "node:fs"; +import { syncBuiltinESMExports } from "node:module"; +import path from "node:path"; import test from "node:test"; +import { pathToFileURL } from "node:url"; import { getTaskSteps } from "../run-tasks.mjs"; @@ -33,3 +38,98 @@ test("retains the explicit SDK Rust formatting command", () => { }, ]); }); + +test("default build selects the existing Node and Rust build steps in order", () => { + assert.deepEqual(getTaskSteps("build:default"), [ + ...getTaskSteps("build", "nodejs"), + ...getTaskSteps("build", "rust"), + ]); + assert.throws(() => getTaskSteps("build:default", "java"), /Unknown SDK task/); +}); + +test("checkout default build prepares both projections once without rebuilding the CLI", async (t) => { + const { runTasks, preparations, calls, sdkRoot, root } = await taskFixture(t, true); + + runTasks("build:default", undefined, { environment: {}, runtimeSource: "checkout" }); + + assert.deepEqual(preparations, [{ languages: ["nodejs", "rust"], runtimeRoot: root, sdkRoot }]); + assert.deepEqual(calls, [ + { executable: "npm", args: ["ci", "--ignore-scripts", "--include=dev"], cwd: path.join(sdkRoot, "nodejs") }, + { executable: "npm", args: ["run", "build"], cwd: path.join(sdkRoot, "nodejs") }, + { + executable: process.platform === "win32" ? "pnpm.cmd" : "pnpm", + args: ["bazel", "build", "--action_env=PATH", "//src/sdk/rust:github-copilot-sdk"], + cwd: root, + }, + ]); + + calls.length = 0; + runTasks("build:default", undefined, { environment: {}, runtimeSource: "checkout" }); + assert.equal(preparations.length, 2); + assert.equal(calls.length, 2); + assert.deepEqual(calls[0].args, ["run", "build"]); +}); + +test("standalone default build retains the all-features Cargo build", async (t) => { + const { runTasks, preparations, calls, sdkRoot } = await taskFixture(t, false); + + runTasks("build:default", undefined, { environment: { COPILOT_RUNTIME_SOURCE: "published" } }); + + assert.deepEqual(preparations, []); + assert.deepEqual(calls, [ + { executable: "npm", args: ["ci", "--ignore-scripts", "--include=dev"], cwd: path.join(sdkRoot, "nodejs") }, + { executable: "npm", args: ["run", "build"], cwd: path.join(sdkRoot, "nodejs") }, + { executable: "cargo", args: ["build", "--all-features"], cwd: path.join(sdkRoot, "rust") }, + ]); +}); + +for (const language of ["nodejs", "rust"]) { + test(`checkout ${language} build still prepares only its selected projection`, async (t) => { + const { runTasks, preparations, calls, sdkRoot, root } = await taskFixture(t, true); + + runTasks("build", language, { environment: {}, runtimeSource: "checkout" }); + + assert.deepEqual(preparations, [{ languages: [language], runtimeRoot: root, sdkRoot }]); + assert.equal(calls.length, language === "nodejs" ? 2 : 1); + assert.ok(calls.every(({ cwd }) => cwd === (language === "nodejs" ? path.join(sdkRoot, language) : root))); + }); +} + +async function taskFixture(t, checkout) { + const root = fs.mkdtempSync(path.join(import.meta.dirname, ".run-tasks-")); + t.after(() => fs.rmSync(root, { recursive: true, force: true })); + const sdkRoot = path.join(root, checkout ? "src/sdk" : "sdk"); + const scripts = path.join(sdkRoot, "scripts"); + fs.mkdirSync(scripts, { recursive: true }); + for (const name of ["run-tasks.mjs", "runtime-layout.mjs", "install-dependencies.mjs"]) { + fs.copyFileSync(new URL(`../${name}`, import.meta.url), path.join(scripts, name)); + } + // Observe the prerequisite boundary without invoking Bazel or changing real generated files. + fs.writeFileSync( + path.join(scripts, "build-prerequisites.mjs"), + `export const preparations = []; +export function prepareSdkSources(options) { preparations.push(options); } +export function bazelActionEnvironmentArgument() { return "--action_env=PATH"; } +`, + ); + fs.mkdirSync(path.join(sdkRoot, "nodejs")); + fs.writeFileSync(path.join(sdkRoot, "nodejs/package.json"), "{}"); + fs.writeFileSync(path.join(sdkRoot, "nodejs/package-lock.json"), "{}"); + if (checkout) { + fs.mkdirSync(path.join(root, "script")); + fs.writeFileSync(path.join(root, "script/sea-build.ts"), ""); + } + const calls = []; + const spawn = t.mock.method(childProcess, "spawnSync", (executable, args, { cwd }) => { + calls.push({ executable, args, cwd }); + return { status: 0 }; + }); + syncBuiltinESMExports(); + t.after(() => { + spawn.mock.restore(); + syncBuiltinESMExports(); + }); + const { runTasks } = await import(pathToFileURL(path.join(scripts, "run-tasks.mjs"))); + const { preparations } = await import(pathToFileURL(path.join(scripts, "build-prerequisites.mjs"))); + return { runTasks, preparations, calls, sdkRoot, root }; +} diff --git a/scripts/ci/runtime-artifact.mjs b/scripts/ci/runtime-artifact.mjs index a6f41275bf..8205c196de 100644 --- a/scripts/ci/runtime-artifact.mjs +++ b/scripts/ci/runtime-artifact.mjs @@ -99,6 +99,8 @@ export function prepareRuntimeArtifact(options = {}) { `package/${executableName}`, "--exclude", "package/app.js", + "--exclude", + "package/cli-main.js", "-czf", path.basename(inProcessArchive), "package", diff --git a/scripts/ci/runtime-artifact.test.mjs b/scripts/ci/runtime-artifact.test.mjs index 329c8dc429..1823844ad3 100644 --- a/scripts/ci/runtime-artifact.test.mjs +++ b/scripts/ci/runtime-artifact.test.mjs @@ -72,6 +72,7 @@ await test("stages all same-checkout runtime inputs", (t) => { const wrapperName = process.platform === "win32" ? "copilot-runtime.exe" : "copilot-runtime"; writeFixture(path.join(runtimeRoot, "dist-cli", "index.js"), "entry point"); writeFixture(path.join(runtimeRoot, "dist-cli", "app.js"), "legacy entry point"); + writeFixture(path.join(runtimeRoot, "dist-cli", "cli-main.js"), "CLI UI"); writeFixture(path.join(runtimeRoot, "dist-cli", "runtime-asset"), "asset"); writeFixture(path.join(runtimeRoot, "dist-cli", "copilot-sdk", "index.js"), "sdk entry point"); writeFixture(path.join(runtimeRoot, "dist-cli", "copilot-sdk", "extension.js"), "sdk extension entry point"); @@ -86,6 +87,7 @@ await test("stages all same-checkout runtime inputs", (t) => { assert.equal(fs.readFileSync(values.COPILOT_RUNTIME_LIBRARY_PATH, "utf8"), "runtime"); assert.equal(fs.readFileSync(values.COPILOT_CLI_PATH, "utf8"), "wrapper"); assert.equal(fs.readFileSync(values.COPILOT_LEGACY_CLI_PATH, "utf8"), "legacy entry point"); + assert.equal(fs.readFileSync(path.join(outputDirectory, "package", "cli-main.js"), "utf8"), "CLI UI"); assert.equal( fs.readFileSync(path.join(values.COPILOT_EXTENSION_SDK_PATH, "extension.js"), "utf8"), "sdk extension entry point", @@ -111,6 +113,7 @@ await test("stages all same-checkout runtime inputs", (t) => { assert.match(inProcessContents.stdout, new RegExp(`package/prebuilds/${target}/runtime\\.node`)); assert.doesNotMatch(inProcessContents.stdout, new RegExp(`^package/${executableName}$`, "m")); assert.doesNotMatch(inProcessContents.stdout, /^package\/app\.js$/m); + assert.doesNotMatch(inProcessContents.stdout, /^package\/cli-main\.js$/m); }); await test("preserves complete checked-in Rust release pins", (t) => { diff --git a/scripts/codegen/csharp.ts b/scripts/codegen/csharp.ts index 6ac39d1f81..1c59d64f18 100644 --- a/scripts/codegen/csharp.ts +++ b/scripts/codegen/csharp.ts @@ -587,6 +587,8 @@ const EXPERIMENTAL_ATTRIBUTE = "[Experimental(global::GitHub.Copilot.Diagnostics const EDITOR_BROWSABLE_NEVER_ATTRIBUTE = "[EditorBrowsable(EditorBrowsableState.Never)]"; const OBSOLETE_ATTRIBUTE = `#if NET5_0_OR_GREATER [Obsolete("This member is deprecated and will be removed in a future version.", DiagnosticId = "GHCP001")] +#else +[Obsolete("This member is deprecated and will be removed in a future version.")] #endif`; const STRING_ENUM_RESERVED_MEMBER_NAMES = new Set(["Value", "Equals", "GetHashCode", "ToString", "Converter"]); @@ -637,6 +639,7 @@ interface EventVariant { dataClassName: string; dataSchema: JSONSchema7; dataDescription?: string; + eventDeprecated: boolean; eventExperimental: boolean; dataExperimental: boolean; } @@ -738,6 +741,7 @@ function extractEventVariants(schema: JSONSchema7): EventVariant[] { dataClassName: `${baseName}Data`, dataSchema, dataDescription: dataSchema?.description, + eventDeprecated: isSchemaDeprecated(variant), eventExperimental: isSchemaExperimental(variant), dataExperimental: isSchemaExperimental(dataSchema), }; @@ -1630,6 +1634,9 @@ namespace GitHub.Copilot; if (variant.eventExperimental) { pushExperimentalAttribute(lines); } + if (variant.eventDeprecated) { + pushObsoleteAttributes(lines); + } lines.push(`public sealed partial class ${variant.className} : SessionEvent`, `{`); lines.push(` /// `); lines.push(` [JsonIgnore]`, ` public override string Type => "${variant.typeName}";`, ""); diff --git a/scripts/codegen/go.ts b/scripts/codegen/go.ts index eb1da853ca..5e01ee7bf2 100644 --- a/scripts/codegen/go.ts +++ b/scripts/codegen/go.ts @@ -505,6 +505,7 @@ interface GoEventVariant { dataClassName: string; dataSchema: JSONSchema7; dataDescription?: string; + eventDeprecated: boolean; eventExperimental: boolean; dataExperimental: boolean; } @@ -598,6 +599,7 @@ function extractGoEventVariants(schema: JSONSchema7): GoEventVariant[] { dataClassName: `${toPascalCase(typeName)}Data`, dataSchema, dataDescription: dataSchema.description, + eventDeprecated: isSchemaDeprecated(variant), eventExperimental: isSchemaExperimental(variant), dataExperimental: isSchemaExperimental(dataSchema), }; @@ -3263,6 +3265,10 @@ export function generateGoSessionEventsCode( if (variant.dataExperimental || isSchemaExperimental(variant.dataSchema)) { pushGoExperimentalTypeComment(lines, variant.dataClassName, ctx); } + if (variant.eventDeprecated || isSchemaDeprecated(variant.dataSchema)) { + lines.push("//"); + pushGoCommentForContext(lines, `Deprecated: ${variant.dataClassName} is deprecated.`, ctx); + } lines.push(`type ${variant.dataClassName} struct {`); const fields: GoStructField[] = []; @@ -3337,6 +3343,9 @@ export function generateGoSessionEventsCode( if (variant?.eventExperimental) { pushGoExperimentalEventComment(eventTypeEnum, constName, "\t"); } + if (variant?.eventDeprecated) { + pushGoComment(eventTypeEnum, `Deprecated: ${constName} identifies a deprecated event.`, "\t"); + } eventTypeEnum.push(`\t${constName} SessionEventType = "${typeName}"`); } eventTypeEnum.push(`)`); @@ -3580,11 +3589,30 @@ function collectGoTopLevelNames(code: string, keyword: "type" | "const"): string return [...names].sort(compareGoTypeNames); } -function generateGoSessionEventAliasFile( +export function collectGoDeprecatedEventNames(sessionSchema: JSONSchema7): ReadonlySet { + const deprecatedNames = new Set(); + for (const variant of extractGoEventVariants(sessionSchema)) { + if (variant.eventDeprecated || isSchemaDeprecated(variant.dataSchema)) { + deprecatedNames.add(variant.dataClassName); + } + if (variant.eventDeprecated) { + deprecatedNames.add( + "SessionEventType" + variant.typeName + .split(/[._]/) + .map((word) => goIdentifierWord(word)) + .join("") + ); + } + } + return deprecatedNames; +} + +export function generateGoSessionEventAliasFile( generatedSessionTypeCode: string, additionalTypeNames: Iterable = [], additionalConstNames: Iterable = [], - excludeTypeNames: Iterable = [] + excludeTypeNames: Iterable = [], + deprecatedNames: ReadonlySet = new Set() ): string { const excluded = new Set(excludeTypeNames); const typeNames = [...new Set([...collectGoTopLevelNames(generatedSessionTypeCode, "type"), ...additionalTypeNames])] @@ -3606,6 +3634,9 @@ function generateGoSessionEventAliasFile( lines.push(`// Session-event types are generated in the rpc package and aliased here for source compatibility.`); lines.push(`type (`); for (const typeName of typeNames) { + if (deprecatedNames.has(typeName)) { + pushGoComment(lines, `Deprecated: ${typeName} is deprecated.`, "\t"); + } lines.push(`\t${typeName} = rpc.${typeName}`); } lines.push(`)`); @@ -3616,6 +3647,9 @@ function generateGoSessionEventAliasFile( lines.push(`// Session-event constants are generated in the rpc package and re-exported here for source compatibility.`); lines.push(`const (`); for (const constName of constNames) { + if (deprecatedNames.has(constName)) { + pushGoComment(lines, `Deprecated: ${constName} identifies a deprecated event.`, "\t"); + } lines.push(`\t${constName} = rpc.${constName}`); } lines.push(`)`); @@ -3873,9 +3907,10 @@ async function generateSessionEvents(schemaPath?: string, apiSchema?: ApiSchema) // aliases that clash with the existing hand-written `copilot.ContextTier`. const handWrittenPublicNames = await collectHandWrittenGoPublicNames(); const aliasExcludes = new Set([...internalTypesInSession, ...handWrittenPublicNames]); + const deprecatedNames = collectGoDeprecatedEventNames(sessionSchema); const aliasOutPath = await writeGeneratedFile( "go/zsession_events.go", - generateGoSessionEventAliasFile(generatedTypeCode, sharedAliasNames.typeNames, sharedAliasNames.constNames, aliasExcludes) + generateGoSessionEventAliasFile(generatedTypeCode, sharedAliasNames.typeNames, sharedAliasNames.constNames, aliasExcludes, deprecatedNames) ); console.log(` ✓ ${aliasOutPath}`); diff --git a/scripts/codegen/python.ts b/scripts/codegen/python.ts index f696fa0b15..3ca936f461 100644 --- a/scripts/codegen/python.ts +++ b/scripts/codegen/python.ts @@ -1636,7 +1636,7 @@ export function appendLastPythonRpcConstructorFields( return updated; } -function reorderPythonDataclassFields(code: string): string { +function reorderPythonDataclassFields(code: string, keywordOnlyLastInClass?: string): string { const fieldRe = /^ \w+: (?:Any|bool|int|float|str|dict|list|ClassVar|[A-Z_]\w*|['"][A-Z_]\w*)(?:[^=]*)?(?: = .*)?$/; const methodRe = /^ (?:@(?:staticmethod|classmethod|property)|(?:async\s+)?def\s+)/; @@ -1677,7 +1677,13 @@ function reorderPythonDataclassFields(code: string): string { const required = groups.filter((group) => !group[0].includes(" = ")); const optional = groups.filter((group) => group[0].includes(" = ")); - const reorderedGroups = [...required, ...optional]; + const reorderedGroups = block.startsWith(`@dataclass\nclass ${keywordOnlyLastInClass}:`) + ? [ + ...required, + ...optional.filter((group) => !group[0].includes("kw_only=True")), + ...optional.filter((group) => group[0].includes("kw_only=True")), + ] + : [...required, ...optional]; const changed = reorderedGroups.some((group, index) => group !== groups[index]); if (!changed) { return block; @@ -2290,6 +2296,17 @@ function getOrCreatePyEnum( } lines.push(` ${toEnumMemberName(value)} = ${JSON.stringify(value)}`); } + if (enumName === "ToolExecutionCompleteFileEditKind") { + lines.push(``); + lines.push(` @classmethod`); + lines.push(` def _missing_(cls, value: object) -> "ToolExecutionCompleteFileEditKind | None":`); + lines.push(` if not isinstance(value, str):`); + lines.push(` return None`); + lines.push(` member = object.__new__(cls)`); + lines.push(` member._name_ = "UNKNOWN"`); + lines.push(` member._value_ = value`); + lines.push(` return cls._value2member_map_.setdefault(value, member)`); + } ctx.enumsByName.set(enumName, enumName); ctx.enums.push(lines.join("\n")); return enumName; @@ -3275,6 +3292,24 @@ async function generateSessionEvents(schemaPath?: string): Promise { // ── RPC Types ─────────────────────────────────────────────────────────────── +function preserveUnknownPythonEnumValues(code: string, name: string): string { + const declaration = new RegExp(`^class ${name}\\(Enum\\):\\n(?:[ \\t].*\\n|\\n)*`, "m"); + const match = code.match(declaration); + if (!match) throw new Error(`Missing generated ${name} enum`); + const method = ` + @classmethod + def _missing_(cls, value: object) -> ${name} | None: + if not isinstance(value, str): + return None + member = object.__new__(cls) + member._name_ = "UNKNOWN" + member._value_ = value + return cls._value2member_map_.setdefault(value, member) + +`; + return code.replace(declaration, `${match[0].trimEnd()}\n${method}`); +} + async function generateRpc(schemaPath?: string, sessionEventsSchema?: JSONSchema7): Promise { console.log("Python: generating RPC types..."); const { FetchingJSONSchemaStore, InputData, JSONSchemaInput, quicktype } = await import("quicktype-core"); @@ -3472,12 +3507,20 @@ async function generateRpc(schemaPath?: string, sessionEventsSchema?: JSONSchema // Reorder class/enum definitions to resolve forward references. // Quicktype may emit classes before their dependencies are defined. typesCode = reorderPythonForwardRefs(typesCode); + for (const name of ["CustomizationReloadStatus", "CustomizationReloadSubsystem"]) { + if (Object.hasOwn(allDefinitions, name)) { + typesCode = preserveUnknownPythonEnumValues(typesCode, name); + } + } typesCode = makePythonDataclassFieldKeywordOnly( typesCode, "MCPServerConfigHTTP", "oauth_scopes" ); typesCode = applyPythonLegacyParameters(typesCode, allDefinitions); + // Preserve the published positional SQLite capability when adding binary reads. + typesCode = makePythonDataclassFieldKeywordOnly(typesCode, "SessionFSSetProviderCapabilities", "binary"); + typesCode = reorderPythonDataclassFields(typesCode, "SessionFSSetProviderCapabilities"); // Strip quicktype's import block and preamble — we provide our own unified header. // The preamble ends just before the first helper function (e.g. "def from_str") diff --git a/scripts/codegen/rust.ts b/scripts/codegen/rust.ts index 31afeae395..0048ceb53a 100644 --- a/scripts/codegen/rust.ts +++ b/scripts/codegen/rust.ts @@ -11,13 +11,13 @@ */ import { execFile } from "child_process"; -import { realpathSync } from "fs"; import fs from "fs/promises"; import path from "path"; import { fileURLToPath } from "url"; import { promisify } from "util"; import type { JSONSchema7, JSONSchema7Definition } from "json-schema"; import { + isCodegenEntrypoint, addManagedApprovalRequiredToPermissionRequests, type ApiSchema, type DefinitionCollections, @@ -1226,6 +1226,10 @@ function emitRustStruct( // ── Enum emission ─────────────────────────────────────────────────────────── +function preservesUnknownStringValue(enumName: string): boolean { + return enumName === "CustomizationReloadStatus" || enumName === "CustomizationReloadSubsystem"; +} + /** * Claims a string enum name. Two const literals resolving to one name with different values * fail generation, so distinct discriminators can never collapse into one enum. A const whose @@ -1261,7 +1265,9 @@ function emitRustStringEnum( ): void { if (!claimRustStringEnum(enumName, values, false, ctx)) return; + const preserveUnknownValue = enumName === "ToolExecutionCompleteFileEditKind"; const lines: string[] = []; + const preserveUnknown = preserveUnknownValue || preservesUnknownStringValue(enumName); if (description) { for (const line of description.split(/\r?\n/)) { lines.push(`/// ${line}`); @@ -1269,12 +1275,18 @@ function emitRustStringEnum( } pushRustExperimentalDocs(lines, experimental || ctx.experimentalTypeNames.has(enumName)); lines.push( - "#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]", + preserveUnknown + ? "#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]" + : "#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]", ); + if (preserveUnknown) { + lines.push('#[serde(from = "String", into = "String")]'); + } lines.push(`pub enum ${enumName} {`); const usedVariantNames = new Set(); const reservedVariantNames = new Set(["Unknown"]); + const variants: { value: string; name: string }[] = []; for (const value of values) { // Keep the protocol's explicit "unknown" distinct from the serde fallback, // including anonymous enums whose names depend on their containing type. @@ -1285,22 +1297,57 @@ function emitRustStringEnum( reservedVariantNames, value === "unknown" ? "UnknownValue" : undefined, ); + variants.push({ value, name: variantName }); pushRustDoc(lines, enumValueDescriptions?.[value], " "); - if (variantName !== value) { + if (!preserveUnknown && variantName !== value) { lines.push(` #[serde(rename = "${value}")]`); } lines.push(` ${variantName},`); } - // Add a catch-all for forward compatibility. This is also the `Default` - // variant — for wire-protocol enums an unknown/sentinel value is the only - // safe default. - lines.push(" /// Unknown variant for forward compatibility."); - lines.push(" #[default]"); - lines.push(" #[serde(other)]"); - lines.push(" Unknown,"); + lines.push( + preserveUnknownValue + ? " /// An unrecognized operation kind, retaining its wire value." + : " /// Unknown variant for forward compatibility.", + ); + if (preserveUnknown) { + lines.push(" Unknown(String),"); + } else { + // For wire-protocol enums an unknown/sentinel value is the only safe default. + lines.push(" #[default]"); + lines.push(" #[serde(other)]"); + lines.push(" Unknown,"); + } lines.push("}"); + if (preserveUnknown) { + if (!preserveUnknownValue) lines.push(""); + lines.push(`impl Default for ${enumName} {`); + lines.push(' fn default() -> Self { Self::Unknown("unknown".to_owned()) }'); + lines.push("}"); + if (!preserveUnknownValue) lines.push(""); + lines.push(`impl From for ${enumName} {`); + lines.push(" fn from(value: String) -> Self {"); + lines.push(" match value.as_str() {"); + for (const variant of variants) { + lines.push(` ${JSON.stringify(variant.value)} => Self::${variant.name},`); + } + lines.push(" _ => Self::Unknown(value),"); + lines.push(" }"); + lines.push(" }"); + lines.push("}"); + if (!preserveUnknownValue) lines.push(""); + lines.push(`impl From<${enumName}> for String {`); + lines.push(` fn from(value: ${enumName}) -> Self {`); + lines.push(" match value {"); + for (const variant of variants) { + lines.push(` ${enumName}::${variant.name} => ${JSON.stringify(variant.value)}.to_owned(),`); + } + lines.push(` ${enumName}::Unknown(value) => value,`); + lines.push(" }"); + lines.push(" }"); + lines.push("}"); + } ctx.enums.push(lines.join("\n")); } @@ -1345,6 +1392,7 @@ interface EventVariant { dataSchema: JSONSchema7; /** Description of the event */ description?: string; + eventDeprecated: boolean; /** Whether the event definition is experimental. */ eventExperimental: boolean; /** Whether the event data definition is experimental. */ @@ -1393,6 +1441,7 @@ function extractEventVariants(schema: JSONSchema7): EventVariant[] { dataClassName: `${toPascalCase(typeName)}Data`, dataSchema, description: resolvedVariant.description || dataSchema.description, + eventDeprecated: isSchemaDeprecated(resolvedVariant), eventExperimental: isSchemaExperimental(resolvedVariant), dataExperimental: isSchemaExperimental(dataSchema), }; @@ -1411,6 +1460,7 @@ export function generateSessionEventsCode(schema: JSONSchema7): string { { allowUntaggedUnions: true, allowedUnionTypeNames: [ + "HumanResponseRecordedResponse", "ToolExecutionCompleteContent", "ToolExecutionCompleteContentResourceDetails", ], @@ -1457,6 +1507,9 @@ export function generateSessionEventsCode(schema: JSONSchema7): string { ); typeEnumLines.push("pub enum SessionEventType {"); for (const variant of variants) { + if (variant.eventDeprecated) { + typeEnumLines.push(...rustDeprecatedAttributes(" ")); + } pushRustExperimentalDocs( typeEnumLines, variant.eventExperimental, @@ -1484,6 +1537,9 @@ export function generateSessionEventsCode(schema: JSONSchema7): string { dataEnumLines.push(`#[serde(tag = "type", content = "data")]`); dataEnumLines.push("pub enum SessionEventData {"); for (const variant of variants) { + if (variant.eventDeprecated) { + dataEnumLines.push(...rustDeprecatedAttributes(" ")); + } pushRustExperimentalDocs( dataEnumLines, variant.dataExperimental, @@ -1662,6 +1718,14 @@ function rustResultTypeName(method: RpcMethod, ctx: RustCodegenCtx): string { recordExternalRustTypeRef(method.result.$ref, ctx); return rustRefTypeName(method.result.$ref); } + if ( + (method.rpcMethod === "session.instructions.reload" && + method.result?.$ref === "#/definitions/InstructionsGetSourcesResult") || + (method.rpcMethod === "session.customizations.reload" && + method.result?.$ref === "#/definitions/CustomizationsReloadResult") + ) { + return rustRefTypeName(method.result.$ref, ctx.definitions); + } return getRpcSchemaTypeName( method.result, `${toPascalCase(method.rpcMethod)}Result`, @@ -1882,7 +1946,6 @@ export function generateApiTypesCode( allowedUnionTypeNames: [ "AuthInfo", "EnqueueCommandResult", - "EntraTokenAcquireResult", "McpOauthProbeResult", "SettableAuthInfo", "ToolResult", @@ -2826,31 +2889,7 @@ async function generate(): Promise { console.log(`Done! Generated files in ${GENERATED_DIR}`); } -const __filename = fileURLToPath(import.meta.url); - -export function isRustCodegenEntrypoint( - entryPath: string | undefined, - modulePath = __filename, - platform = process.platform, -): boolean { - if (!entryPath) { - return false; - } - const canonicalize = (filePath: string) => { - try { - return realpathSync.native(filePath); - } catch { - return path.resolve(filePath); - } - }; - const canonicalEntryPath = canonicalize(entryPath); - const canonicalModulePath = canonicalize(modulePath); - return platform === "win32" - ? canonicalEntryPath.toLowerCase() === canonicalModulePath.toLowerCase() - : canonicalEntryPath === canonicalModulePath; -} - -if (isRustCodegenEntrypoint(process.argv[1])) { +if (isCodegenEntrypoint(process.argv[1], fileURLToPath(import.meta.url))) { generate().catch((err) => { console.error("Code generation failed:", err); process.exit(1); diff --git a/scripts/codegen/typescript.ts b/scripts/codegen/typescript.ts index 8f46864b20..9cce8743ee 100644 --- a/scripts/codegen/typescript.ts +++ b/scripts/codegen/typescript.ts @@ -385,6 +385,9 @@ export function normalizeSchemaForTypeScript( root.definitions = definitions; delete root.$defs; + const openReloadEnums = new Set( + ["CustomizationReloadStatus", "CustomizationReloadSubsystem"].map((name) => definitions[name]) + ); const internalDefinitionNames = new Set( Object.entries(definitions) .filter(([, definition]) => typeof definition === "object" && definition !== null && isSchemaInternal(definition as JSONSchema7)) @@ -439,14 +442,19 @@ export function normalizeSchemaForTypeScript( } const enumValueDescriptions = getEnumValueDescriptions(rewritten as JSONSchema7); - if (enumValueDescriptions && Array.isArray(rewritten.enum) && rewritten.enum.every((entry) => typeof entry === "string")) { - rewritten.tsType = (rewritten.enum as string[]) + if ((enumValueDescriptions || openReloadEnums.has(source)) && Array.isArray(rewritten.enum) && rewritten.enum.every((entry) => typeof entry === "string")) { + const documentedValues = (rewritten.enum as string[]) .map((entry) => { - const comment = enumValueDescriptions[entry]; + const comment = enumValueDescriptions?.[entry]; const literal = JSON.stringify(entry); return comment ? `${tsDocCommentText(comment)}\n| ${literal}` : `| ${literal}`; }) .join("\n"); + // Preserve future wire values without losing completion for the known edit kinds. + rewritten.tsType = + rewritten.title === "ToolExecutionCompleteFileEditKind" || openReloadEnums.has(source) + ? `${documentedValues}\n| (string & {})` + : documentedValues; delete rewritten.type; delete rewritten.enum; delete rewritten["x-enumDescriptions"]; diff --git a/scripts/install-dependencies.mjs b/scripts/install-dependencies.mjs new file mode 100644 index 0000000000..123e822c08 --- /dev/null +++ b/scripts/install-dependencies.mjs @@ -0,0 +1,36 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +import { createHash } from "node:crypto"; +import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs"; +import { join } from "node:path"; + +export function installNpmDependencies(directory, runNpm) { + const nodeModules = join(directory, "node_modules"); + const stampFile = join(nodeModules, ".copilot-sdk-install-stamp"); + const args = ["ci", "--ignore-scripts", "--include=dev"]; + const hash = createHash("sha256").update(JSON.stringify(args)); + for (const file of ["package-lock.json", "package.json"]) { + hash.update(readFileSync(join(directory, file))); + } + const fingerprint = hash.digest("hex"); + let stamp; + try { + stamp = readFileSync(stampFile, "utf8").trim(); + } catch (error) { + if (error.code !== "ENOENT") { + throw error; + } + } + if (existsSync(nodeModules) && stamp === fingerprint) { + return false; + } + + // Failed installs must not leave a stamp claiming the dependency tree is current. + rmSync(stampFile, { force: true }); + runNpm(args, directory); + mkdirSync(nodeModules, { recursive: true }); + writeFileSync(stampFile, `${fingerprint}\n`); + return true; +} diff --git a/scripts/run-tasks.mjs b/scripts/run-tasks.mjs index a74fafbf14..63ca1b77e2 100644 --- a/scripts/run-tasks.mjs +++ b/scripts/run-tasks.mjs @@ -10,6 +10,7 @@ import { spawnSync } from "node:child_process"; import { fileURLToPath } from "node:url"; import { bazelActionEnvironmentArgument, prepareSdkSources } from "./build-prerequisites.mjs"; import { findRuntimeRoot, getRuntimeCliPaths } from "./runtime-layout.mjs"; +import { installNpmDependencies } from "./install-dependencies.mjs"; const sdkRoot = resolve(dirname(fileURLToPath(import.meta.url)), ".."); const languages = ["nodejs", "python", "go", "dotnet", "java", "rust"]; @@ -35,7 +36,7 @@ const tasks = { "generate:schemas": {}, build: { nodejs: [ - command("nodejs", "npm", ["ci", "--ignore-scripts"]), + command("nodejs", "npm", ["ci", "--ignore-scripts", "--include=dev"]), command("nodejs", "npm", ["run", "build"]), ], python: [ @@ -151,6 +152,11 @@ tasks["test:default"] = { rust: tasks.test.rust, }; +tasks["build:default"] = { + nodejs: tasks.build.nodejs, + rust: tasks.build.rust, +}; + tasks.check.rust = [...tasks["format:check"].rust, ...tasks.lint.rust]; function collectGoFiles(directory) { @@ -304,7 +310,7 @@ export function runTasks(verb, language, options = {}) { if ( runtimeSource === "checkout" && runtimeRoot && - ["build", "generate", "generate:schemas", "test", "test:default"].includes(verb) + ["build", "build:default", "generate", "generate:schemas", "test", "test:default"].includes(verb) ) { const selectedLanguages = language ? [language] : Object.keys(tasks[verb]); prepareSdkSources({ languages: selectedLanguages, runtimeRoot, sdkRoot }); @@ -341,13 +347,21 @@ export function runTasks(verb, language, options = {}) { if (step.kind === "gofmt") { runGoFormat(step.write); } else { - if (resolvedRuntimeSource === "checkout" && verb === "build" && step.language === "rust") { + if ( + resolvedRuntimeSource === "checkout" && + (verb === "build" || verb === "build:default") && + step.language === "rust" + ) { runRootCommand( runtimeRoot, "pnpm", ["bazel", "build", bazelActionEnvironmentArgument(environment), "//src/sdk/rust:github-copilot-sdk"], environment, ); + } else if (step.executable === "npm" && step.args[0] === "ci") { + installNpmDependencies(resolve(sdkRoot, step.cwd), (args) => + runCommand({ ...step, args }, environment), + ); } else { runCommand(step, environment); } diff --git a/test/harness/modelProtocolAdapters.test.ts b/test/harness/modelProtocolAdapters.test.ts index 6773edca80..a7b6ab4ce2 100644 --- a/test/harness/modelProtocolAdapters.test.ts +++ b/test/harness/modelProtocolAdapters.test.ts @@ -303,6 +303,40 @@ describe("Anthropic Messages adapter", () => { }); describe("OpenAI Responses adapter", () => { + test("normalizes image-bearing function output without changing ordinary JSON output", () => { + const imageOutput = JSON.stringify([ + { type: "input_text", text: "Viewed image file successfully." }, + { + type: "input_image", + detail: "auto", + image_url: "data:image/png;base64,AQID", + }, + ]); + const result = JSON.parse( + responsesApiRequestToChatCompletion( + JSON.stringify({ + model: "test-model", + input: [ + { + type: "function_call_output", + call_id: "view-1", + output: imageOutput, + }, + { + type: "function_call_output", + call_id: "other-1", + output: '[{"ok":true}]', + }, + ], + }), + ), + ) as { messages: Array<{ content: string }> }; + expect(result.messages.map((message) => message.content)).toEqual([ + "Viewed image file successfully.", + '[{"ok":true}]', + ]); + }); + test("normalizes messages, binary content, and tools", () => { const result = JSON.parse( responsesApiRequestToChatCompletion( diff --git a/test/harness/replayingCapiProxy.test.ts b/test/harness/replayingCapiProxy.test.ts index fd2d037da8..1092d19f10 100644 --- a/test/harness/replayingCapiProxy.test.ts +++ b/test/harness/replayingCapiProxy.test.ts @@ -15,6 +15,8 @@ import { afterEach, beforeEach, describe, expect, test, vi } from "vitest"; import yaml from "yaml"; import { NormalizedData, + type ReplayBackend, + ParsedHttpExchange, ReplayingCapiProxy, ToolResultNormalizer, workingDirPlaceholder, @@ -158,6 +160,32 @@ describe("ReplayingCapiProxy", () => { await expect(readFile(outputPath)).rejects.toThrow(/ENOENT/); }); + test("uses configured model display names and resets them between tests", async () => { + const proxy = new ReplayingCapiProxy("http://localhost"); + const address = await proxy.start(); + const config = { filePath: path.join(tempDir, "models.yaml"), workDir }; + try { + await proxy.updateConfig({ + ...config, + modelNames: { "claude-sonnet-5": "Claude Sonnet 5" }, + }); + const named = await fetch(`${address}/models`); + expect(named.status).toBe(200); + expect(await named.json()).toMatchObject({ + data: [{ id: "claude-sonnet-5", name: "Claude Sonnet 5" }], + }); + + await proxy.updateConfig(config); + const reset = await fetch(`${address}/models`); + expect(reset.status).toBe(200); + expect(await reset.json()).toMatchObject({ + data: [{ id: "claude-sonnet-5", name: "claude-sonnet-5" }], + }); + } finally { + await proxy.stop(); + } + }); + test("captures chat completion request and response", async () => { const requestBody = JSON.stringify({ messages: [ @@ -182,6 +210,545 @@ describe("ReplayingCapiProxy", () => { ]); }); + test.each([ + [false, 37], + [true, 37], + [false, 0], + [true, 0], + [false, undefined], + [true, undefined], + [false, null], + [true, null], + ] as const)( + "exposes provider input usage for compaction assertions (streaming: %s, input: %s)", + async (streaming, inputTokens) => { + const summary = "Completed summary"; + const usage = { + prompt_tokens: inputTokens, + completion_tokens: 5, + total_tokens: (inputTokens ?? 0) + 5, + }; + const response = { + id: "compaction-response", + object: streaming ? "chat.completion.chunk" : "chat.completion", + created: 1, + model: "test-model", + choices: [ + { + index: 0, + ...(streaming + ? { delta: { role: "assistant", content: summary } } + : { message: { role: "assistant", content: summary } }), + finish_reason: "stop", + logprobs: null, + }, + ], + usage, + }; + const proxy = new ReplayingCapiProxy( + "http://localhost", + path.join(tempDir, "compaction.yaml"), + workDir, + ); + (proxy.exchanges as Array).push({ + request: { + url: "/chat/completions", + method: "POST", + body: JSON.stringify({ messages: [], model: "test-model" }), + headers: { + "x-interaction-type": "conversation-compaction", + "x-interaction-id": "compaction-interaction", + }, + }, + response: { + statusCode: 200, + body: streaming + ? `data: ${JSON.stringify(response)}\n\ndata: [DONE]\n\n` + : JSON.stringify(response), + }, + }); + const address = await proxy.start(); + try { + const result = await fetch(`${address}/exchanges`); + expect(result.ok).toBe(true); + const exchanges = (await result.json()) as ParsedHttpExchange[]; + const compactionResponses = exchanges.filter( + (exchange) => + exchange.response?.choices && + exchange.requestHeaders?.["x-interaction-type"] === + "conversation-compaction", + ); + expect(compactionResponses).toHaveLength(1); + expect( + compactionResponses[0].response?.choices + .map((choice) => choice.message.content ?? "") + .join(""), + ).toBe(summary); + expect( + compactionResponses[0].response?.usage?.prompt_tokens ?? undefined, + ).toBe(inputTokens ?? undefined); + expect(compactionResponses[0].compactionUsage).toEqual({ + interactionId: "compaction-interaction", + summary, + responseCount: 1, + ...(inputTokens == null ? {} : { inputTokens }), + }); + } finally { + await proxy.stop(true); + } + }, + ); + + test.each([ + [ + "reasoning-only", + null, + 11, + "Completed summary", + 37, + true, + 48, + ], + [ + "split text", + "", + 11, + "Completed summary", + 37, + true, + 48, + true, + ], + [ + "missing first input", + null, + undefined, + "Completed summary", + 37, + true, + undefined, + ], + [ + "null final input", + null, + 11, + "Completed summary", + null, + true, + undefined, + ], + [ + "explicit zero", + null, + 0, + "Completed summary", + 0, + true, + 0, + ], + [ + "new attempt", + null, + 11, + "Completed summary", + 37, + false, + 37, + ], + ] as const)( + "correlates compaction provider usage across %s", + async ( + _name, + firstContent, + firstInput, + finalContent, + finalInput, + continuation, + expectedInput, + multipartContinuation: boolean = false, + ) => { + const proxy = new ReplayingCapiProxy( + "http://localhost", + path.join(tempDir, "compaction.yaml"), + workDir, + ); + const responses = [ + { content: firstContent, inputTokens: firstInput }, + { content: finalContent, inputTokens: finalInput }, + ]; + for (const [index, { content, inputTokens }] of responses.entries()) { + (proxy.exchanges as Array).push({ + request: { + url: "/chat/completions", + method: "POST", + body: JSON.stringify({ + messages: [ + { + role: "user", + content: + index === 1 && continuation + ? multipartContinuation + ? [ + { + type: "text", + text: "clock\n", + }, + { + type: "text", + text: "Please continue from where you left off.", + }, + ] + : "Please continue from where you left off." + : "Summarize the conversation.", + }, + ], + model: "test-model", + }), + headers: { + "x-interaction-type": "conversation-compaction", + "x-interaction-id": "compaction-interaction", + }, + }, + response: { + statusCode: 200, + body: JSON.stringify({ + choices: [ + { + message: { + role: "assistant", + content, + reasoning_content: + content === null ? "Reasoning before summary" : undefined, + }, + finish_reason: + index === 0 && content !== null ? "length" : "stop", + }, + ], + usage: { prompt_tokens: inputTokens, completion_tokens: 5 }, + }), + }, + }); + if (index === 0) { + for (const [interactionType, interactionId, statusCode] of [ + ["conversation", "compaction-interaction", 200], + ["conversation-compaction", "other-interaction", 200], + ["conversation-compaction", "compaction-interaction", 500], + ]) { + (proxy.exchanges as Array).push({ + request: { + url: "/chat/completions", + method: "POST", + body: JSON.stringify({ messages: [], model: "test-model" }), + headers: { + "x-interaction-type": interactionType, + "x-interaction-id": interactionId, + }, + }, + response: { + statusCode, + body: JSON.stringify({ + choices: [ + { + message: { + role: "assistant", + content: "Unrelated response", + }, + }, + ], + usage: { prompt_tokens: 999 }, + }), + }, + }); + } + } + } + const address = await proxy.start(); + try { + const result = await fetch(`${address}/exchanges`); + expect(result.ok).toBe(true); + const exchanges = (await result.json()) as ParsedHttpExchange[]; + const compactions = exchanges.filter( + (exchange) => + exchange.compactionUsage?.summary === + "Completed summary", + ); + expect(compactions).toHaveLength(1); + expect(compactions[0].compactionUsage).toEqual({ + interactionId: "compaction-interaction", + summary: "Completed summary", + responseCount: continuation ? 2 : 1, + ...(expectedInput === undefined + ? {} + : { inputTokens: expectedInput }), + }); + } finally { + await proxy.stop(true); + } + }, + ); + + test.each(["object", "scalar", "freeform"] as const)( + "preserves opaque tool text while normalizing and replaying paths (%s)", + async (argumentKind) => { + const opaqueText = String.raw`word\b before\nafter ${workingDirPlaceholder}`; + const patchBody = [ + String.raw`+const pattern = /word\b/;`, + String.raw`+const text = "before\nafter";`, + String.raw`+const escapes = "\r\t\b\u0041";`, + `+const root = "${workDir}\\literal\\b";`, + `+const placeholder = "${workingDirPlaceholder}";`, + String.raw`+*** Add File: body\must\stay.txt`, + ].join("\n"); + const patch = [ + "*** Begin Patch", + `*** Add File: ${workDir}\\src\\marker.txt`, + patchBody, + String.raw`*** Update File: src\before.txt`, + String.raw`*** Move to: src\after.txt`, + "@@", + "-before", + "+after", + String.raw`*** Delete File: src\obsolete.txt`, + "*** End Patch", + ].join("\n"); + const expectedPatch = (root: string) => + [ + "*** Begin Patch", + `*** Add File: ${root}/src/marker.txt`, + patchBody, + "*** Update File: src/before.txt", + "*** Move to: src/after.txt", + "@@", + "-before", + "+after", + "*** Delete File: src/obsolete.txt", + "*** End Patch", + ].join("\n"); + const patchArguments = (input: string, filename: string) => + argumentKind === "object" + ? JSON.stringify({ input, path: filename, content: opaqueText }) + : argumentKind === "scalar" + ? JSON.stringify(input) + : input; + const textArguments = (root: string, separator: string) => + JSON.stringify({ + input: opaqueText, + content: opaqueText, + prompt: opaqueText, + paths: [ + `src${separator}one.txt`, + `${root}${separator}src${separator}two.txt`, + ], + }); + const request = { + model: "test-model", + messages: [{ role: "user", content: "Apply the patch" }], + }; + const outputPath = await createProxy([ + { + url: "/chat/completions", + requestBody: JSON.stringify(request), + responseBody: JSON.stringify({ + choices: [ + { + message: { + role: "assistant", + tool_calls: [ + { + id: "patch-call", + type: "function", + function: { + name: "apply_patch", + arguments: patchArguments(patch, "src\\marker.txt"), + }, + }, + { + id: "text-call", + type: "function", + function: { + name: "literal_tool", + arguments: textArguments(workDir, "\\"), + }, + }, + { + id: "scalar-call", + type: "function", + function: { + name: "scalar_tool", + arguments: JSON.stringify(opaqueText), + }, + }, + ], + }, + }, + ], + }), + }, + ]); + + const result = await readYamlOutput(outputPath); + expect( + result.conversations[0].messages[1].tool_calls?.map( + (call) => call.function?.arguments, + ), + ).toEqual([ + patchArguments(expectedPatch(workingDirPlaceholder), "src/marker.txt"), + textArguments(workingDirPlaceholder, "/"), + JSON.stringify(opaqueText), + ]); + + const proxy = new ReplayingCapiProxy("http://localhost:1"); + await proxy.updateConfig({ + filePath: outputPath, + workDir, + backend: "capi", + replayOnly: true, + }); + const proxyUrl = await proxy.start(); + try { + const response = await fetch(`${proxyUrl}/chat/completions`, { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ ...request, stream: false }), + }); + expect(response.status).toBe(200); + const completion = (await response.json()) as ChatCompletion; + expect( + completion.choices[0].message.tool_calls?.map((call) => + call.type === "function" ? call.function.arguments : undefined, + ), + ).toEqual([ + patchArguments(expectedPatch(workDir), "src/marker.txt"), + textArguments(workDir, "/"), + JSON.stringify(opaqueText), + ]); + } finally { + await proxy.stop(true); + } + }, + ); + + test.each(["/", "\\"])( + "normalizes and replays delegated prompts across working directories (%s)", + async (separator) => { + const promptForDir = (root: string, pathSeparator: string) => + `Read ${root}${pathSeparator}src${pathSeparator}marker.txt`; + const taskArguments = (prompt: string) => + JSON.stringify({ + agent_type: "explore", + description: "Read marker.txt", + prompt, + }); + const parentRequest = { + model: "test-model", + messages: [{ role: "user", content: "Delegate the file read" }], + }; + const outputPath = await createProxy([ + { + url: "/chat/completions", + requestBody: JSON.stringify(parentRequest), + responseBody: JSON.stringify({ + choices: [ + { + message: { + role: "assistant", + tool_calls: [ + { + id: "task-call", + type: "function", + function: { + name: "task", + arguments: taskArguments( + promptForDir(workDir, separator), + ), + }, + }, + ], + }, + }, + ], + }), + }, + { + url: "/chat/completions", + requestBody: JSON.stringify({ + model: "test-model", + messages: [ + { role: "user", content: promptForDir(workDir, separator) }, + ], + }), + responseBody: JSON.stringify({ + choices: [ + { + message: { + role: "assistant", + content: "Delegated file read completed.", + }, + }, + ], + }), + }, + ]); + const result = await readYamlOutput(outputPath); + const normalizedPrompt = promptForDir(workingDirPlaceholder, "/"); + expect(result.conversations).toHaveLength(2); + expect( + result.conversations[0].messages[1].tool_calls?.[0].function?.arguments, + ).toBe(taskArguments(normalizedPrompt)); + expect(result.conversations[1].messages[0].content).toBe( + normalizedPrompt, + ); + + const replayWorkDir = path.join(tempDir, "replay-work"); + const proxy = new ReplayingCapiProxy("http://localhost:1"); + await proxy.updateConfig({ + filePath: outputPath, + workDir: replayWorkDir, + backend: "capi", + replayOnly: true, + }); + const proxyUrl = await proxy.start(); + try { + const parentResponse = await fetch(`${proxyUrl}/chat/completions`, { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ ...parentRequest, stream: false }), + }); + expect(parentResponse.status).toBe(200); + const parentCompletion = + (await parentResponse.json()) as ChatCompletion; + const taskCall = parentCompletion.choices[0].message.tool_calls?.[0]; + expect(taskCall?.type).toBe("function"); + if (taskCall?.type !== "function") { + throw new Error("Expected a delegated task tool call"); + } + expect(taskCall.function.name).toBe("task"); + expect(taskCall.function.arguments).toBe( + taskArguments(promptForDir(replayWorkDir, "/")), + ); + const { prompt } = JSON.parse(taskCall.function.arguments) as { + prompt: string; + }; + const childResponse = await fetch(`${proxyUrl}/chat/completions`, { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ + model: "test-model", + messages: [{ role: "user", content: prompt }], + stream: false, + }), + }); + expect(childResponse.status).toBe(200); + const childCompletion = (await childResponse.json()) as ChatCompletion; + expect(childCompletion.choices[0].message.content).toBe( + "Delegated file read completed.", + ); + } finally { + await proxy.stop(true); + } + }, + ); + test("normalizes tool call IDs to sequential values", async () => { const requestBody = JSON.stringify({ messages: [{ role: "user", content: "Do something" }], @@ -397,6 +964,70 @@ describe("ReplayingCapiProxy", () => { expect(result.conversations[0].messages[0].content).toBe("What is 2+2?"); }); + test("strips mode_changed_notice from user messages", async () => { + const requestBody = JSON.stringify({ + messages: [ + { + role: "user", + content: + "Context before.\n\n\nWrite a plan only.\n\n\nCreate a brief implementation plan.", + }, + ], + }); + const responseBody = JSON.stringify({ + choices: [{ message: { role: "assistant", content: "Plan ready" } }], + }); + + const outputPath = await createProxy([ + { url: "/chat/completions", requestBody, responseBody }, + ]); + + const result = await readYamlOutput(outputPath); + expect(result.conversations[0].messages[0].content).toBe( + "Context before.\n\nCreate a brief implementation plan.", + ); + }); + + test("drops notice-only user turns while preserving genuinely empty input", async () => { + const responseBody = JSON.stringify({ + choices: [{ message: { role: "assistant", content: "Ready" } }], + }); + const noticeOutputPath = await createProxy([ + { + url: "/chat/completions", + requestBody: JSON.stringify({ + messages: [ + { + role: "user", + content: + "\nPlan mode is no longer active.\n", + }, + ], + }), + responseBody, + }, + ]); + const noticeResult = await readYamlOutput(noticeOutputPath); + expect(noticeResult.conversations[0].messages).toEqual([ + { role: "assistant", content: "Ready" }, + ]); + + const emptyOutputPath = await createProxy([ + { + url: "/chat/completions", + requestBody: JSON.stringify({ + messages: [{ role: "user", content: "" }], + }), + responseBody, + }, + ]); + const emptyResult = await readYamlOutput(emptyOutputPath); + expect(emptyResult.conversations[0].messages).toEqual([ + { role: "user" }, + { role: "assistant", content: "Ready" }, + ]); + }); + test("strips plan mode prefix from user messages", async () => { const requestBody = JSON.stringify({ messages: [ @@ -729,47 +1360,49 @@ Always include PINEAPPLE_COCONUT_42. expect(toolMessage?.content).toBe(stableResult); }); - test("normalizes read_agent result metadata", async () => { - const requestBody = JSON.stringify({ - messages: [ - { role: "user", content: "Help me" }, - { - role: "assistant", - tool_calls: [ - { - id: "tc1", - type: "function", - function: { - name: "read_agent", - arguments: '{"agent_id":"read-file","wait":true}', + test.each(["", ", model: claude-sonnet-5"])( + "normalizes read_agent result metadata%s", + async (model) => { + const requestBody = JSON.stringify({ + messages: [ + { role: "user", content: "Help me" }, + { + role: "assistant", + tool_calls: [ + { + id: "tc1", + type: "function", + function: { + name: "read_agent", + arguments: '{"agent_id":"read-file","wait":true}', + }, }, - }, - ], - }, - { - role: "tool", - tool_call_id: "tc1", - content: - "Agent is idle (waiting for messages). agent_id: read-file, agent_type: explore, status: idle, description: Reading subagent-test.txt, elapsed: 1.25s, total_turns: 1\n\n[Turn 0]\nDone.", - }, - ], - }); - const responseBody = JSON.stringify({ - choices: [{ message: { role: "assistant", content: "Done" } }], - }); + ], + }, + { + role: "tool", + tool_call_id: "tc1", + content: `Agent is idle (waiting for messages). agent_id: read-file, agent_type: explore, status: idle, description: Reading subagent-test.txt, elapsed: 1.25s, total_turns: 1${model}\n\n[Turn 0]\nDone.`, + }, + ], + }); + const responseBody = JSON.stringify({ + choices: [{ message: { role: "assistant", content: "Done" } }], + }); - const outputPath = await createProxy([ - { url: "/chat/completions", requestBody, responseBody }, - ]); + const outputPath = await createProxy([ + { url: "/chat/completions", requestBody, responseBody }, + ]); - const result = await readYamlOutput(outputPath); - const toolMessage = result.conversations[0].messages.find( - (m) => m.role === "tool", - ); - expect(toolMessage?.content).toBe( - "Agent completed. agent_id: read-file, agent_type: explore, status: completed, description: Reading subagent-test.txt, elapsed: 0s, total_turns: 0, duration: 0s\n\nDone.", - ); - }); + const result = await readYamlOutput(outputPath); + const toolMessage = result.conversations[0].messages.find( + (m) => m.role === "tool", + ); + expect(toolMessage?.content).toBe( + "Agent completed. agent_id: read-file, agent_type: explore, status: completed, description: Reading subagent-test.txt, elapsed: 0s, total_turns: 0, duration: 0s\n\nDone.", + ); + }, + ); test("names runtime agent IDs that no task call introduced", async () => { const runtimeAgentId = "3e0c7565-6091-58cb-85bb-6cb14db23ef7"; @@ -916,6 +1549,103 @@ Always include PINEAPPLE_COCONUT_42. }); } + test.each([ + ["scripts/check.cjs", "references/policy.txt"], + ["references/policy.txt", "scripts/check.cjs"], + ])( + "replays OTel skill resource fixtures independently of enumeration order %s %s", + async (first, second) => { + const cachePath = path.join(tempDir, "skill-order.yaml"); + const context = (root: string, files: string[]) => + ` +Base directory for this skill: ${root} + +Related files (use view tool to read): +${files.map((file) => ` - ${root}/${file}`).join("\n")} + +Follow the user's explicit instructions. +Related files (use view tool to read): + - authored-second + - authored-first +`; + await writeFile( + cachePath, + yaml.stringify({ + models: ["test-model"], + conversations: [ + { + messages: [ + { + role: "user", + content: + "\nWrite a plan only.\n\n\n" + + context(`${workingDirPlaceholder}/skills/review`, [ + first, + second, + ]), + }, + { role: "assistant", content: "OTEL_RESOURCE_REPLAY_DONE" }, + ], + }, + ], + } satisfies NormalizedData), + ); + const proxy = new ReplayingCapiProxy("http://localhost"); + await proxy.updateConfig({ + filePath: cachePath, + workDir, + replayOnly: true, + }); + const proxyUrl = await proxy.start(); + try { + const request = { + model: "test-model", + messages: [ + { + role: "user", + content: + context(path.join(workDir, "skills", "review"), [ + second, + first, + ]) + + "\n\n\nPlan mode is no longer active.\n", + }, + ], + }; + const response = await makeRequest(proxyUrl, "/chat/completions", { + body: request, + }); + expect(response.status).toBe(200); + expect(JSON.parse(response.body).choices[0].message.content).toBe( + "OTEL_RESOURCE_REPLAY_DONE", + ); + + for (const content of [ + context(path.join(workDir, "skills", "review"), [first]), + context(path.join(workDir, "skills", "review"), [ + first, + second, + second, + ]), + request.messages[0].content.replace( + " - authored-second\n - authored-first", + " - authored-first\n - authored-second", + ), + ]) { + const mismatch = await makeRequest(proxyUrl, "/chat/completions", { + body: { + model: "test-model", + messages: [{ role: "user", content }], + }, + }); + expect(mismatch.status).toBe(500); + } + } finally { + await proxy.stop(true); + } + }, + ); + test("replay-only mode rejects cache misses without contacting the upstream", async () => { let upstreamRequests = 0; const upstream = http.createServer((_request, response) => { @@ -967,6 +1697,107 @@ Always include PINEAPPLE_COCONUT_42. } }); + test.each([ + "capi", + "openai-completions", + "openai-responses", + "anthropic-messages", + ])( + "replays a notice-only stored turn while preserving empty user input through %s", + async (backend) => { + const cachePath = path.join(tempDir, `mode-notice-${backend}.yaml`); + await writeFile( + cachePath, + yaml.stringify({ + models: ["test-model"], + conversations: [ + { + messages: [ + { + role: "user", + content: + "\nPlan mode is no longer active.\n", + }, + { role: "assistant", content: "Notice ready" }, + ], + }, + { + messages: [ + { role: "user" }, + { role: "assistant", content: "Empty ready" }, + ], + }, + ], + } satisfies NormalizedData), + ); + const proxy = new ReplayingCapiProxy("http://localhost:9999"); + await proxy.updateConfig({ + filePath: cachePath, + workDir, + backend, + replayOnly: true, + }); + const proxyUrl = await proxy.start(); + const request = (content: string) => { + switch (backend) { + case "capi": + case "openai-completions": + return { + endpoint: "/chat/completions", + body: { + model: "test-model", + messages: [{ role: "user", content }], + }, + }; + case "openai-responses": + return { + endpoint: "/responses", + body: { + model: "test-model", + input: [ + { + type: "message", + role: "user", + content: [{ type: "input_text", text: content }], + }, + ], + }, + }; + case "anthropic-messages": + return { + endpoint: "/v1/messages", + body: { + model: "test-model", + max_tokens: 100, + messages: [{ role: "user", content }], + }, + }; + } + }; + + try { + const noticeRequest = request( + "\nPlan mode is no longer active.\n", + ); + const response = await makeRequest(proxyUrl, noticeRequest.endpoint, { + body: noticeRequest.body, + }); + + expect(response.status).toBe(200); + expect(response.body).toContain("Notice ready"); + + const emptyRequest = request(""); + const emptyResponse = await makeRequest(proxyUrl, emptyRequest.endpoint, { + body: emptyRequest.body, + }); + expect(emptyResponse.status).toBe(200); + expect(emptyResponse.body).toContain("Empty ready"); + } finally { + await proxy.stop(true); + } + }, + ); + test.each([ ["should_accept_blob_attachments", "pixel.png"], ["vision_disabled_then_enabled_via_setmodel", "test.png"], @@ -1164,6 +1995,127 @@ Always include PINEAPPLE_COCONUT_42. } }); + test("replays a CAPI view-image capture for BYOK tool-result and image-turn continuations", async () => { + const cachePath = path.join(tempDir, "view-image.yaml"); + await writeFile( + cachePath, + yaml.stringify({ + models: ["test-model"], + conversations: [ + { + messages: [ + { role: "system", content: "${system}" }, + { role: "user", content: "View the image" }, + { + role: "assistant", + tool_calls: [ + { + id: "toolcall_0", + type: "function", + function: { + name: "view", + arguments: '{"path":"/image.png"}', + }, + }, + ], + }, + { + role: "tool", + tool_call_id: "toolcall_0", + content: "Viewed image file successfully.", + }, + { role: "user", content: "Image file at path /image.png\n[image]" }, + { role: "assistant", content: "Image viewed" }, + ], + }, + ], + } satisfies NormalizedData), + ); + const proxy = new ReplayingCapiProxy("http://localhost:9999"); + await proxy.updateConfig({ + filePath: cachePath, + workDir, + backend: "openai-completions", + replayOnly: true, + }); + const proxyUrl = await proxy.start(); + + try { + const response = await makeRequest(proxyUrl, "/chat/completions", { + body: { + model: "test-model", + messages: [ + { role: "system", content: "System prompt" }, + { role: "user", content: "View the image" }, + { + role: "assistant", + tool_calls: [ + { + id: "runtime-call-id", + type: "function", + function: { name: "view", arguments: '{"path":"/image.png"}' }, + }, + ], + }, + { + role: "tool", + tool_call_id: "runtime-call-id", + content: "Viewed image file successfully.", + }, + ], + }, + }); + + expect(response.status).toBe(200); + expect((JSON.parse(response.body) as ChatCompletion).choices[0].message.content).toBe( + "Image viewed", + ); + + const visionResponse = await makeRequest( + proxyUrl, + "/chat/completions", + { + body: { + model: "test-model", + messages: [ + { role: "system", content: "System prompt" }, + { role: "user", content: "View the image" }, + { + role: "assistant", + tool_calls: [ + { + id: "runtime-call-id", + type: "function", + function: { + name: "view", + arguments: '{"path":"/image.png"}', + }, + }, + ], + }, + { + role: "tool", + tool_call_id: "runtime-call-id", + content: "Viewed image file successfully.", + }, + { + role: "user", + content: "Image file at path /image.png\n[image]", + }, + ], + }, + }, + ); + expect(visionResponse.status).toBe(200); + expect( + (JSON.parse(visionResponse.body) as ChatCompletion).choices[0].message + .content, + ).toBe("Image viewed"); + } finally { + await proxy.stop(true); + } + }); + test("matches shell tool results with shell ID completion markers", async () => { const originalShellConfig = process.platform === "win32" @@ -1974,7 +2926,9 @@ Always include PINEAPPLE_COCONUT_42. "magic_number", ]); expect(calls.map((call) => call.index)).toEqual([0, 1]); - expect(JSON.parse(calls[1].function!.arguments!)).toEqual({ seed: "hello" }); + expect(JSON.parse(calls[1].function!.arguments!)).toEqual({ + seed: "hello", + }); expect(chunks.at(-1)?.choices[0].finish_reason).toBe("tool_calls"); } finally { await proxy.stop(); diff --git a/test/harness/replayingCapiProxy.ts b/test/harness/replayingCapiProxy.ts index 8c680e03d5..dd5627df3a 100644 --- a/test/harness/replayingCapiProxy.ts +++ b/test/harness/replayingCapiProxy.ts @@ -202,6 +202,7 @@ export class ReplayingCapiProxy extends CapturingHttpProxy { filePath: config.filePath, workDir: config.workDir, testInfo: config.testInfo, + modelNames: config.modelNames, backend: parseReplayBackend(config.backend), replayOnly: config.replayOnly === true, autoResponseIndex: 0, @@ -335,20 +336,22 @@ export class ReplayingCapiProxy extends CapturingHttpProxy { options.requestOptions.method === "GET" ) { const protocol = replayProtocols[this.state?.backend ?? "capi"]; + const modelExchanges = this.exchanges.filter( + (exchange) => exchange.request.url === protocol.endpoint, + ); const parsedExchanges = await Promise.all( - this.exchanges - .filter((exchange) => exchange.request.url === protocol.endpoint) - .map((exchange) => - parseHttpExchange( - protocol.normalizeRequest?.(exchange.request.body) ?? - exchange.request.body, - protocol.canonicalResponse - ? exchange.response?.body - : undefined, - exchange.request.headers, - ), + modelExchanges.map((exchange) => + parseHttpExchange( + protocol.normalizeRequest?.(exchange.request.body) ?? + exchange.request.body, + protocol.canonicalResponse + ? exchange.response?.body + : undefined, + exchange.request.headers, ), + ), ); + addCompactionProviderUsage(parsedExchanges, modelExchanges); options.onResponseStart(200, {}); options.onData(Buffer.from(JSON.stringify(parsedExchanges))); options.onResponseEnd(); @@ -443,7 +446,7 @@ export class ReplayingCapiProxy extends CapturingHttpProxy { state.storedData?.models && state.storedData.models.length > 0 ? state.storedData.models : [defaultModel]; - const modelsResponse = createGetModelsResponse(models); + const modelsResponse = createGetModelsResponse(models, state.modelNames); const body = JSON.stringify(modelsResponse); const headers = { "content-type": "application/json", @@ -1058,6 +1061,14 @@ function coalesceAdjacentUserMessages(requestBody: string): string { const messages: NonNullable = []; for (const message of request.messages) { + if ( + message.role === "user" && + typeof message.content === "string" && + containsModeChangedNotice(message.content) && + normalizeUserMessage(message.content) === "" + ) { + continue; + } const previous = messages.at(-1); if ( previous?.role === "user" && @@ -1089,6 +1100,70 @@ function openAIErrorBody(code: string | undefined, message: string): unknown { return { error: { message, type, code: type } }; } +function rewriteToolArgumentPaths( + toolName: string, + argumentsJson: string, + rewritePath: (value: string) => string, + rewriteTextWorkDir: (value: string) => string, +): string { + const rewritePatchPaths = (patch: string) => + patch.replace( + /^(\*\*\* (?:Add File|Update File|Delete File|Move to):[ \t]*)([^\r\n]*)/gm, + (_match, header: string, filename: string) => + header + rewritePath(filename), + ); + if ( + toolName === "apply_patch" && + argumentsJson.trimStart().startsWith("*** Begin Patch") + ) { + return rewritePatchPaths(argumentsJson); + } + + let argumentsValue: unknown; + try { + argumentsValue = JSON.parse(argumentsJson, (key, value: unknown) => { + const isPath = /^(?:path|paths|file_path|filePath|cwd|directory)$/.test( + key, + ); + if (typeof value === "string") { + if (toolName === "apply_patch" && (key === "" || key === "input")) { + return rewritePatchPaths(value); + } + if (isPath) { + return rewritePath(value); + } + if (toolName === "${shell}" && key === "command") { + return rewriteTextWorkDir(value); + } + } else if (isPath && Array.isArray(value)) { + return value.map((item: unknown) => + typeof item === "string" ? rewritePath(item) : item, + ); + } + return value; + }); + } catch (error) { + if (error instanceof SyntaxError) { + return argumentsJson; + } + throw error; + } + if ( + toolName === "task" && + typeof argumentsValue === "object" && + argumentsValue !== null && + "prompt" in argumentsValue && + typeof argumentsValue.prompt === "string" + ) { + argumentsValue.prompt = rewriteTextWorkDir(argumentsValue.prompt); + } + const rewritten = JSON.stringify(argumentsValue); + if (rewritten === undefined) { + throw new Error("Unable to encode rewritten tool arguments"); + } + return rewritten; +} + function normalizeFilenames( conversations: NormalizedConversation[], workDir: string, @@ -1112,6 +1187,10 @@ function normalizeFilenames( /(? path.replace(/\\/g, "/"); + const normalizePath = (value: string) => + value.replace(workDirPattern, workDirReplacer).replace(/\\/g, "/"); + const normalizeTextWorkDir = (value: string) => + value.replace(workDirPattern, workDirReplacer); for (const conv of conversations) { for (const msg of conv.messages) { @@ -1121,13 +1200,11 @@ function normalizeFilenames( } for (const tc of msg.tool_calls ?? []) { if (tc.function?.arguments) { - tc.function.arguments = tc.function.arguments.replace( - workDirPattern, - workDirReplacer, - ); - tc.function.arguments = tc.function.arguments.replace( - windowsFnPattern, - windowsFnReplacer, + tc.function.arguments = rewriteToolArgumentPaths( + tc.function.name, + tc.function.arguments, + normalizePath, + normalizeTextWorkDir, ); } } @@ -1331,6 +1408,77 @@ async function parseHttpExchange( return { request, response, requestHeaders }; } +function addCompactionProviderUsage( + exchanges: ParsedHttpExchange[], + capturedExchanges: readonly CapturedExchange[], +): void { + const chains = new Map< + string, + { + usage: CompactionProviderUsage; + previous: ParsedHttpExchange; + } + >(); + for (const [index, exchange] of exchanges.entries()) { + const status = capturedExchanges[index].response?.statusCode; + const headers = exchange.requestHeaders; + if ( + status === undefined || + status < 200 || + status >= 300 || + headers?.["x-interaction-type"] !== "conversation-compaction" || + !exchange.response?.choices + ) { + continue; + } + const interactionId = headers["x-interaction-id"]; + if (typeof interactionId !== "string" || !interactionId) { + throw new Error( + "Compaction provider response has no interaction identity", + ); + } + const lastMessage = exchange.request.messages.at(-1); + const lastContent = + typeof lastMessage?.content === "string" + ? lastMessage.content + : lastMessage?.role === "user" && Array.isArray(lastMessage.content) + ? lastMessage.content + .map((part) => (part.type === "text" ? part.text : "")) + .join("") + : ""; + // The runtime appends this nudge for both length and reasoning-only continuations. + const continuation = + lastMessage?.role === "user" && + lastContent.endsWith("Please continue from where you left off."); + let chain = continuation ? chains.get(interactionId) : undefined; + if (continuation && !chain) { + throw new Error( + "Compaction continuation has no captured preceding provider response", + ); + } + if (!chain) { + chain = { + usage: { interactionId, summary: "", responseCount: 0, inputTokens: 0 }, + previous: exchange, + }; + chains.set(interactionId, chain); + } else { + delete chain.previous.compactionUsage; + } + const inputTokens = exchange.response.usage?.prompt_tokens; + chain.usage.inputTokens = + typeof inputTokens === "number" && chain.usage.inputTokens !== undefined + ? chain.usage.inputTokens + inputTokens + : undefined; + chain.usage.summary += exchange.response.choices + .map((choice) => choice.message.content ?? "") + .join(""); + chain.usage.responseCount++; + exchange.compactionUsage = { ...chain.usage }; + chain.previous = exchange; + } +} + // Converts a single HTTP exchange (request + response) into a normalized conversation async function transformHttpExchange( requestBody: string, @@ -1340,7 +1488,9 @@ async function transformHttpExchange( requestBody, responseBody, ); - const messages = request.messages.map(transformOpenAIRequestMessage); + const messages = request.messages + .map(transformOpenAIRequestMessage) + .filter((message): message is NormalizedMessage => message !== undefined); if (response?.choices?.length) { messages.push(...transformOpenAIResponseChoice(response.choices)); @@ -1353,29 +1503,41 @@ async function transformHttpExchange( // We use this to look up whether we already have a cached response for it function transformOpenAIRequestMessage( m: ChatCompletionMessageParam, -): NormalizedMessage { +): NormalizedMessage | undefined { let content: string | undefined; + let strippedModeNoticeOnly = false; if (m.role === "system") { // System message changes too often to include in snapshots - just store placeholder content = "${system}"; } else if (m.role === "user" && typeof m.content === "string") { content = normalizeUserMessage(m.content); + strippedModeNoticeOnly = + content === "" && containsModeChangedNotice(m.content); } else if (m.role === "user" && Array.isArray(m.content)) { // Multimodal user messages have array content with text and image_url parts. // Extract and normalize text parts; represent image_url parts as a stable marker. const parts: string[] = []; + let sawModeNotice = false; + let sawVisibleContent = false; for (const part of m.content) { if ( typeof part === "object" && part.type === "text" && typeof part.text === "string" ) { - parts.push(normalizeUserMessage(part.text)); + sawModeNotice ||= containsModeChangedNotice(part.text); + const normalized = normalizeUserMessage(part.text); + if (normalized) { + parts.push(normalized); + sawVisibleContent = true; + } } else if (typeof part === "object" && part.type === "image_url") { parts.push("[image]"); + sawVisibleContent = true; } } content = parts.join("\n") || undefined; + strippedModeNoticeOnly = sawModeNotice && !sawVisibleContent; } else if (m.role === "tool" && typeof m.content === "string") { // If it's a JSON tool call result, normalize the whitespace and property ordering. // For successful tool results wrapped in {resultType, textResultForLlm}, unwrap to @@ -1402,6 +1564,9 @@ function transformOpenAIRequestMessage( content = m.content; } + if (strippedModeNoticeOnly) { + return undefined; + } const msg: NormalizedMessage = { role: m.role }; if ("tool_call_id" in m && m.tool_call_id) { msg.tool_call_id = m.tool_call_id; @@ -1414,7 +1579,7 @@ function transformOpenAIRequestMessage( } function normalizeUserMessage(content: string): string { - return normalizeSkillContextFrontmatter(content) + return stripModeChangedNotice(normalizeSkillContext(content)) .replace( taskCompletionNotificationPattern, taskCompletionNotificationReplacement, @@ -1436,16 +1601,54 @@ const taskCompletionNotificationPattern = const taskCompletionNotificationReplacement = 'Agent "$1" ($2) has completed successfully. Use read_agent with agent_id "$1" to retrieve the full results.'; +const modeChangedNoticePattern = + /(\s*)[\s\S]*?<\/mode_changed_notice>(\s*)/g; + +function containsModeChangedNotice(content: string): boolean { + return /[\s\S]*?<\/mode_changed_notice>/.test(content); +} + +function stripModeChangedNotice(content: string): string { + let removed = false; + const stripped = content.replace( + modeChangedNoticePattern, + ( + match, + leading: string, + trailing: string, + offset: number, + source: string, + ) => { + removed = true; + const before = source.slice(0, offset); + const after = source.slice(offset + match.length); + if (!before.trim() || !after.trim()) { + return ""; + } + return /[\r\n]/.test(leading + trailing) ? "\n\n" : " "; + }, + ); + return removed ? stripped.trim() : content; +} + function normalizeStoredUserMessages(conversations: NormalizedConversation[]) { for (const conversation of conversations) { for (const message of conversation.messages) { if (message.role === "user" && typeof message.content === "string") { - message.content = message.content.replace( - taskCompletionNotificationPattern, - taskCompletionNotificationReplacement, + message.content = stripModeChangedNotice( + normalizeSkillContext(message.content).replace( + taskCompletionNotificationPattern, + taskCompletionNotificationReplacement, + ), ); } } + conversation.messages = conversation.messages.filter( + (message) => + message.role !== "user" || + message.content !== "" || + Object.keys(message).length !== 2, + ); } } @@ -1508,12 +1711,19 @@ function normalizeStoredToolMessages(conversations: NormalizedConversation[]) { } } -function normalizeSkillContextFrontmatter(content: string): string { +function normalizeSkillContext(content: string): string { // Runtime versions may include or omit SKILL.md metadata in the prompt context. - return content.replace( - /(]*>\s*Base directory for this skill:[^\r\n]*(?:\r?\n)+)---\r?\n(?:(?!<\/skill-context>)[\s\S])*?\r?\n---(?:\r?\n)+/g, - "$1", - ); + return content + .replace( + /(]*>\s*Base directory for this skill:[^\r\n]*(?:\r?\n)+)---\r?\n(?:(?!<\/skill-context>)[\s\S])*?\r?\n---(?:\r?\n)+/g, + "$1", + ) + .replace( + // Filesystem enumeration order is not part of the OTel skill contract. + /(]*>\s*Base directory for this skill:[^\r\n]*(?:\r?\n)+Related files \(use view tool to read\):\r?\n)((?: - [^\r\n]+\r?\n)+)/g, + (_match, prefix: string, files: string) => + prefix + files.split(/\r?\n/).slice(0, -1).sort().join("\n") + "\n", + ); } function normalizeLargeOutputFilepaths(result: string): string { @@ -1599,6 +1809,7 @@ function normalizeReadAgentResult(result: string): string { /^Agent completed\. (.*), status: idle,/, "Agent completed. $1, status: completed,", ) + .replace(/(, total_turns: \d+), model: [^,\r\n]+/, "$1") .replace(/, total_turns: \d+(?=\r?\n|$)/, ", total_turns: 0, duration: 0s") .replace(/\r?\n\r?\n\[Turn \d+\]\r?\n/, "\n\n"); @@ -1971,8 +2182,33 @@ function findAssistantIndexAfterPrefix( // A continuation can start after an assistant message. Never coalesce // across this candidate request/response boundary. + const savedPrefix = savedMessages.slice(0, nextIndex); + const requestHasRelocatedImage = requestMessages.some( + (message, index) => + message.role === "user" && + /^Image file at path [^\n]+\n\[image\]$/.test(message.content ?? "") && + requestMessages[index - 1]?.role === "tool", + ); const prefix = normalizeMessagesForBackend( - savedMessages.slice(0, nextIndex), + backend === "capi" || requestHasRelocatedImage + ? savedPrefix + : savedPrefix.filter((message, index) => { + const toolResult = savedPrefix[index - 1]; + const toolCall = savedPrefix[index - 2]; + // CAPI sends viewed images as a separate user turn; BYOK includes + // them in (or, for text-only models, omits them from) the tool result. + return !( + message.role === "user" && + /^Image file at path [^\n]+\n\[image\]$/.test( + message.content ?? "", + ) && + toolResult?.role === "tool" && + toolResult.content === "Viewed image file successfully." && + toolCall?.tool_calls?.some( + (call) => call.function?.name === "view", + ) + ); + }), backend, ); if (prefix.length > requestMessages.length) break; @@ -1996,21 +2232,6 @@ function findAssistantIndexAfterPrefix( return undefined; } -function expandWorkDir( - content: string | undefined, - workDir: string, - jsonEscape: boolean, -): string | undefined { - if (!content) { - return content; - } - - const workDirValue = jsonEscape - ? JSON.stringify(workDir).replaceAll('"', "") - : workDir; - return content.replace(/\$\{workdir\}/g, workDirValue); -} - function expandToolName(name: string): string { for (const [fullName, normalized] of Object.entries(normalizedToolNames)) { if (name === normalized) { @@ -2029,6 +2250,8 @@ function createOpenAIResponse( workDir: string, backgroundAgentIds: Map, ): ChatCompletion { + const expandPath = (value: string) => + value.replace(/\$\{workdir\}/g, () => workDir); // Here we recreate the strange CAPI/productcode behavior of using multiple choices to represent // multiple assistant messages in a row. This is the inverse of the logic in transformOpenAIResponseChoice(). // So, find all successive assistant messages starting from responseStartIndex. @@ -2045,7 +2268,12 @@ function createOpenAIResponse( function: { name: expandToolName(tc.function?.name ?? ""), arguments: expandBackgroundAgentArguments( - expandWorkDir(tc.function?.arguments, workDir, true) ?? "{}", + rewriteToolArgumentPaths( + tc.function?.name ?? "", + tc.function?.arguments ?? "{}", + expandPath, + expandPath, + ), backgroundAgentIds, ), }, @@ -2055,8 +2283,9 @@ function createOpenAIResponse( index, message: { role: "assistant", - content: - expandWorkDir(assistantMessage.content, workDir, false) ?? null, + content: assistantMessage.content + ? expandPath(assistantMessage.content) + : null, refusal: assistantMessage.refusal ?? null, tool_calls: toolCalls, }, @@ -2092,7 +2321,8 @@ function convertToStreamingResponseChunks( const toolCalls = completion.choices .flatMap((choice) => choice.message.tool_calls ?? []) .filter( - (tc): tc is ChatCompletionMessageFunctionToolCall => tc.type === "function", + (tc): tc is ChatCompletionMessageFunctionToolCall => + tc.type === "function", ); const makeChunk = ( @@ -2146,19 +2376,22 @@ function convertToStreamingResponseChunks( } chunks[chunks.length - 1].choices[0].finish_reason = toolCalls.length ? "tool_calls" - : completion.choices.at(-1)?.finish_reason ?? "stop"; + : (completion.choices.at(-1)?.finish_reason ?? "stop"); return chunks; } -function createGetModelsResponse(modelIds: string[]) { +function createGetModelsResponse( + modelIds: string[], + modelNames?: Record, +) { // Obviously the following might not match any given model. We could track the original responses from /models, // but that risks invalidating the caches too frequently and making this unmaintainable. If this approximation // turns out to be insufficient, we can tweak the logic here based on known model IDs. return { data: modelIds.map((id) => ({ id, - name: id, + name: modelNames?.[id] ?? id, capabilities: { supports: { vision: true }, limits: { max_context_window_tokens: 128000 }, @@ -2245,6 +2478,15 @@ export type ParsedHttpExchange = { request: ChatCompletionCreateParamsBase; response: ChatCompletion | undefined; requestHeaders?: Record; + /** Aggregated provider usage on the last successful response of a compaction chain. */ + compactionUsage?: CompactionProviderUsage; +}; + +export type CompactionProviderUsage = { + interactionId: string; + summary: string; + responseCount: number; + inputTokens?: number; }; // We want to be able to reuse the proxy across multiple tests, so it needs to be reconfigurable @@ -2253,6 +2495,7 @@ type ReplayingCapiProxyState = { filePath: string; workDir: string; testInfo?: { file: string; line?: number }; + modelNames?: Record; backend: ReplayBackend; replayOnly: boolean; storedData?: NormalizedData | undefined; diff --git a/test/harness/responsesApiAdapter.ts b/test/harness/responsesApiAdapter.ts index 16568f6e03..68eceb6df6 100644 --- a/test/harness/responsesApiAdapter.ts +++ b/test/harness/responsesApiAdapter.ts @@ -102,10 +102,7 @@ function responseInputItemToCanonicalMessages( { role: "tool", tool_call_id: typeof item.call_id === "string" ? item.call_id : "", - content: - typeof item.output === "string" - ? item.output - : JSON.stringify(item.output ?? ""), + content: normalizeFunctionCallOutput(item.output), }, ]; } @@ -180,6 +177,35 @@ function responseInputItemToCanonicalMessages( ]; } +function normalizeFunctionCallOutput(output: unknown): string { + const text = + typeof output === "string" ? output : JSON.stringify(output ?? ""); + if (!text.startsWith("[{") || !text.includes('"input_image"')) return text; + + let parts: unknown; + try { + parts = JSON.parse(text); + } catch { + return text; + } + if ( + !Array.isArray(parts) || + !parts.some((part) => isObject(part) && part.type === "input_image") || + !parts.every( + (part) => + isObject(part) && + ((part.type === "input_text" && typeof part.text === "string") || + (part.type === "input_image" && typeof part.image_url === "string")), + ) + ) { + return text; + } + return parts + .filter((part) => part.type === "input_text") + .map((part) => part.text) + .join("\n"); +} + function coalesceAssistantMessages( messages: CanonicalMessage[], ): CanonicalMessage[] { diff --git a/test/harness/subagentHooksReplay.test.ts b/test/harness/subagentHooksReplay.test.ts index 904e4b95a9..9a71371b30 100644 --- a/test/harness/subagentHooksReplay.test.ts +++ b/test/harness/subagentHooksReplay.test.ts @@ -195,7 +195,7 @@ for (const timing of ["before", "after"] as const) { notification, ], continuation, - ...(timing === "before" + ...(timing === "before" && notification === rawNotification ? [[...prefix, notification, readAgent, toolResult]] : []), [ @@ -267,8 +267,39 @@ for (const timing of ["before", "after"] as const) { ); } +for (const streaming of [false, true]) { + test(`replays an idle-first read_agent call without an interim reply, streaming=${streaming}`, async () => { + const proxy = new ReplayingCapiProxy( + "http://127.0.0.1:1", + snapshotPath, + import.meta.dirname, + ); + const url = await proxy.start(); + try { + const response = await fetch(`${url}/chat/completions`, { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ + model: stored.models[0], + messages: [ + ...original.slice(0, 5), + shortIdleNotification, + shortReadAgent, + toolResult, + ], + stream: streaming, + }), + }); + expect(response.status, await response.clone().text()).toBe(200); + expectReply(await readReply(response, streaming), finalAnswer); + } finally { + await proxy.stop(true); + } + }); +} + test("timing and wording alternatives retain the full result and final continuation", () => { - expect(stored.conversations).toHaveLength(7); + expect(stored.conversations).toHaveLength(8); expect(stored.conversations[4].messages).toEqual([ ...original.slice(0, 5), notification, @@ -299,4 +330,75 @@ test("timing and wording alternatives retain the full result and final continuat }), ); } + expect(stored.conversations[7].messages).toEqual([ + ...original.slice(0, 5), + shortIdleNotification, + shortReadAgent, + toolResult, + finalAnswer, + ]); +}); + +test("lifecycle output capture replays the changed child prompt and parent result", async () => { + const variantPath = path.join( + import.meta.dirname, + "..", + "snapshots", + "subagent_hooks", + "should_apply_subagent_lifecycle_hook_outputs.yaml", + ); + const variant = yaml.parse( + await readFile(variantPath, "utf8"), + ) as NormalizedData; + expect(variant.conversations).toHaveLength(stored.conversations.length); + for (let i = 0; i < stored.conversations.length; i++) { + expect( + variant.conversations[i].messages.filter( + (message) => message.role === "assistant", + ), + ).toEqual( + stored.conversations[i].messages.filter( + (message) => message.role === "assistant", + ), + ); + } + const child = variant.conversations[1].messages; + expect(child[1].content).toBe( + `Subagent start hook verified: read the requested file.\n\n${stored.conversations[1].messages[1].content}`, + ); + const parent = variant.conversations[3].messages; + expect(parent.at(-2)?.content).toContain( + "\n\nSubagent stop hook verified: The complete contents", + ); + const idleFirstParent = variant.conversations[7].messages; + expect(idleFirstParent).toEqual([ + ...parent.slice(0, 5), + shortIdleNotification, + shortReadAgent, + parent.at(-2), + parent.at(-1), + ]); + + const proxy = new ReplayingCapiProxy( + "http://127.0.0.1:1", + variantPath, + import.meta.dirname, + ); + const url = await proxy.start(); + try { + for (const messages of [ + child.slice(0, 2), + parent.slice(0, -1), + idleFirstParent.slice(0, -1), + ]) { + const response = await fetch(`${url}/chat/completions`, { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ model: variant.models[0], messages }), + }); + expect(response.status, await response.clone().text()).toBe(200); + } + } finally { + await proxy.stop(true); + } }); diff --git a/test/snapshots/mcp_instruction_recovery/should_recover__bluebird__instructions_without_withdrawing_peer_tools_mid_turn.yaml b/test/snapshots/mcp_instruction_recovery/should_recover__bluebird__instructions_without_withdrawing_peer_tools_mid_turn.yaml new file mode 100644 index 0000000000..c39ed6f7c6 --- /dev/null +++ b/test/snapshots/mcp_instruction_recovery/should_recover__bluebird__instructions_without_withdrawing_peer_tools_mid_turn.yaml @@ -0,0 +1,47 @@ +models: + - claude-sonnet-5 +conversations: + - messages: + - role: system + content: ${system} + - role: user + content: Call reconnect_catalog exactly once. After it returns, call read_record on bluebird. then call read_record on + healthy. Do not use other tools or invent their outputs. Finish by reporting both record values and + CATALOG_RECOVERY_COMPLETE. + - role: assistant + tool_calls: + - id: toolcall_0 + type: function + function: + name: reconnect_catalog + arguments: "{}" + - role: tool + tool_call_id: toolcall_0 + content: Catalog reconnected. Read both server records now. + - role: assistant + tool_calls: + - id: toolcall_1 + type: function + function: + name: bluebird-read_record + arguments: "{}" + - role: tool + tool_call_id: toolcall_1 + content: CATALOG_RECORD + - role: assistant + tool_calls: + - id: toolcall_2 + type: function + function: + name: healthy-read_record + arguments: "{}" + - role: tool + tool_call_id: toolcall_2 + content: PEER_RECORD + - role: assistant + content: |- + Results: + - bluebird record: CATALOG_RECORD + - healthy record: PEER_RECORD + + CATALOG_RECOVERY_COMPLETE diff --git a/test/snapshots/mcp_instruction_recovery/should_recover__workiq__instructions_without_withdrawing_peer_tools_mid_turn.yaml b/test/snapshots/mcp_instruction_recovery/should_recover__workiq__instructions_without_withdrawing_peer_tools_mid_turn.yaml new file mode 100644 index 0000000000..33db4e3314 --- /dev/null +++ b/test/snapshots/mcp_instruction_recovery/should_recover__workiq__instructions_without_withdrawing_peer_tools_mid_turn.yaml @@ -0,0 +1,36 @@ +models: + - claude-sonnet-5 +conversations: + - messages: + - role: system + content: ${system} + - role: user + content: Call read_record on workiq. If it reports a changed tool catalog, retry it. then call read_record on healthy. + Do not use other tools or invent their outputs. Finish by reporting both record values and + CATALOG_RECOVERY_COMPLETE. + - role: assistant + tool_calls: + - id: toolcall_0 + type: function + function: + name: workiq-read_record + arguments: "{}" + - role: tool + tool_call_id: toolcall_0 + content: CATALOG_RECORD + - role: assistant + tool_calls: + - id: toolcall_1 + type: function + function: + name: healthy-read_record + arguments: "{}" + - role: tool + tool_call_id: toolcall_1 + content: PEER_RECORD + - role: assistant + content: |- + workiq record: CATALOG_RECORD + healthy record: PEER_RECORD + + CATALOG_RECOVERY_COMPLETE diff --git a/test/snapshots/permissions/should_wait_for_slow_permission_handler.yaml b/test/snapshots/permissions/should_wait_for_slow_permission_handler.yaml index 9ecbdb6a51..6c51792210 100644 --- a/test/snapshots/permissions/should_wait_for_slow_permission_handler.yaml +++ b/test/snapshots/permissions/should_wait_for_slow_permission_handler.yaml @@ -12,7 +12,8 @@ conversations: type: function function: name: ${shell} - arguments: "{\"command\":\"echo slow_handler_test\",\"description\":\"Echo 'slow_handler_test' to output\"}" + arguments: "{\"command\":\"echo slow_handler_test\",\"description\":\"Echo 'slow_handler_test' to + output\",\"initial_wait\":120}" - role: tool tool_call_id: toolcall_0 content: |- diff --git a/test/snapshots/rpc_tasks_and_handlers/should_apply_a_custom_agent_s_composed_model_and_effort_when_started_over_rpc.yaml b/test/snapshots/rpc_tasks_and_handlers/should_apply_a_custom_agent_s_composed_model_and_effort_when_started_over_rpc.yaml new file mode 100644 index 0000000000..e125a53cec --- /dev/null +++ b/test/snapshots/rpc_tasks_and_handlers/should_apply_a_custom_agent_s_composed_model_and_effort_when_started_over_rpc.yaml @@ -0,0 +1,10 @@ +models: + - claude-sonnet-5 +conversations: + - messages: + - role: system + content: ${system} + - role: user + content: Reply with SDK_COMPOSED_AGENT_DONE exactly. + - role: assistant + content: SDK_COMPOSED_AGENT_DONE diff --git a/test/snapshots/rpc_tasks_and_handlers/should_preserve_a_composed_display_name_effort_in_a_model_issued_task.yaml b/test/snapshots/rpc_tasks_and_handlers/should_preserve_a_composed_display_name_effort_in_a_model_issued_task.yaml new file mode 100644 index 0000000000..0666f899dd --- /dev/null +++ b/test/snapshots/rpc_tasks_and_handlers/should_preserve_a_composed_display_name_effort_in_a_model_issued_task.yaml @@ -0,0 +1,31 @@ +models: + - claude-sonnet-5 +conversations: + - messages: + - role: system + content: ${system} + - role: user + content: Reply with SDK_DISPLAY_AGENT_DONE exactly. + - role: assistant + content: SDK_DISPLAY_AGENT_DONE + - messages: + - role: system + content: ${system} + - role: user + content: Use the task tool with agent_type "sdk-display-model-agent", mode "sync", and prompt "Reply with + SDK_DISPLAY_AGENT_DONE exactly.". Do not specify model or reasoning_effort in the task arguments. After it + completes, reply with SDK_DISPLAY_PARENT_DONE exactly. + - role: assistant + tool_calls: + - id: toolcall_0 + type: function + function: + name: task + arguments: '{"name":"sdk-display-model-agent","prompt":"Reply with SDK_DISPLAY_AGENT_DONE + exactly.","agent_type":"sdk-display-model-agent","mode":"sync","description":"Run SDK display model + agent"}' + - role: tool + tool_call_id: toolcall_0 + content: SDK_DISPLAY_AGENT_DONE + - role: assistant + content: SDK_DISPLAY_PARENT_DONE diff --git a/test/snapshots/sandbox_devices/does_not_offer_a_sandbox_bypass_for_standard_device_paths.yaml b/test/snapshots/sandbox_devices/does_not_offer_a_sandbox_bypass_for_standard_device_paths.yaml new file mode 100644 index 0000000000..df012e6650 --- /dev/null +++ b/test/snapshots/sandbox_devices/does_not_offer_a_sandbox_bypass_for_standard_device_paths.yaml @@ -0,0 +1,25 @@ +models: + - claude-sonnet-5 +conversations: + - messages: + - role: system + content: ${system} + - role: user + content: "Use the bash tool exactly once to run this exact command: `head -c 16 /dev/urandom > entropy.bin && cat + /dev/stdin < denial.txt > copy.txt && cat copy.txt`. Do not request a sandbox bypass. After the tool returns, + reply with exactly DEVICE_COMMAND_COMPLETE." + - role: assistant + tool_calls: + - id: toolcall_0 + type: function + function: + name: ${shell} + arguments: '{"command":"head -c 16 /dev/urandom > entropy.bin && cat /dev/stdin < denial.txt > copy.txt && cat + copy.txt","description":"Run specified exact command"}' + - role: tool + tool_call_id: toolcall_0 + content: |- + cat: /dev/stdin: Operation not permitted + + - role: assistant + content: DEVICE_COMMAND_COMPLETE diff --git a/test/snapshots/session_fs/should_view_an_image_that_exists_only_in_the_binary_session_fs_provider.yaml b/test/snapshots/session_fs/should_view_an_image_that_exists_only_in_the_binary_session_fs_provider.yaml new file mode 100644 index 0000000000..a7f369961c --- /dev/null +++ b/test/snapshots/session_fs/should_view_an_image_that_exists_only_in_the_binary_session_fs_provider.yaml @@ -0,0 +1,24 @@ +models: + - claude-sonnet-5 +conversations: + - messages: + - role: system + content: ${system} + - role: user + content: Use the view tool to view /sdk-provider-image.png, then reply with exactly SDK_PROVIDER_IMAGE_DONE. + - role: assistant + tool_calls: + - id: toolcall_0 + type: function + function: + name: view + arguments: '{"path":"/sdk-provider-image.png"}' + - role: tool + tool_call_id: toolcall_0 + content: Viewed image file successfully. + - role: user + content: |- + Image file at path /sdk-provider-image.png + [image] + - role: assistant + content: SDK_PROVIDER_IMAGE_DONE diff --git a/test/snapshots/session_fs_sqlite/should_allow_subagents_to_use_sql_tool_via_inherited_sessionfs.yaml b/test/snapshots/session_fs_sqlite/should_allow_subagents_to_use_sql_tool_via_inherited_sessionfs.yaml index b2b50348f5..574188c4ae 100644 --- a/test/snapshots/session_fs_sqlite/should_allow_subagents_to_use_sql_tool_via_inherited_sessionfs.yaml +++ b/test/snapshots/session_fs_sqlite/should_allow_subagents_to_use_sql_tool_via_inherited_sessionfs.yaml @@ -81,9 +81,7 @@ conversations: for the SQL operation (e.g., \\\"Insert test todo\\\").\",\"mode\":\"sync\"}" - role: tool tool_call_id: toolcall_0 - content: Tool 'report_intent' does not exist. Available tools that can be called are ${shell}, ${read_shell}, - ${stop_shell}, ${list_shell}, view, create, edit, web_fetch, skill, sql, read_agent, list_agents, grep, glob, - task. + content: Tool 'report_intent' does not exist. - role: tool tool_call_id: toolcall_1 content: >- diff --git a/test/snapshots/session_fs_sqlite/should_route_sql_queries_through_the_sessionfs_sqlite_handler.yaml b/test/snapshots/session_fs_sqlite/should_route_sql_queries_through_the_sessionfs_sqlite_handler.yaml index e51bc5d247..4b26151860 100644 --- a/test/snapshots/session_fs_sqlite/should_route_sql_queries_through_the_sessionfs_sqlite_handler.yaml +++ b/test/snapshots/session_fs_sqlite/should_route_sql_queries_through_the_sessionfs_sqlite_handler.yaml @@ -42,9 +42,7 @@ conversations: arguments: '{"description":"Create items table","query":"CREATE TABLE items (id TEXT PRIMARY KEY, name TEXT)"}' - role: tool tool_call_id: toolcall_0 - content: Tool 'report_intent' does not exist. Available tools that can be called are ${shell}, ${read_shell}, - ${stop_shell}, ${list_shell}, view, create, edit, web_fetch, skill, sql, read_agent, list_agents, grep, glob, - task. + content: Tool 'report_intent' does not exist. - role: tool tool_call_id: toolcall_1 content: Schema operation completed successfully. @@ -78,9 +76,7 @@ conversations: arguments: '{"description":"Create items table","query":"CREATE TABLE items (id TEXT PRIMARY KEY, name TEXT)"}' - role: tool tool_call_id: toolcall_0 - content: Tool 'report_intent' does not exist. Available tools that can be called are ${shell}, ${read_shell}, - ${stop_shell}, ${list_shell}, view, create, edit, web_fetch, skill, sql, read_agent, list_agents, grep, glob, - task. + content: Tool 'report_intent' does not exist. - role: tool tool_call_id: toolcall_1 content: Schema operation completed successfully. diff --git a/test/snapshots/subagent_hooks/should_apply_subagent_lifecycle_hook_outputs.yaml b/test/snapshots/subagent_hooks/should_apply_subagent_lifecycle_hook_outputs.yaml new file mode 100644 index 0000000000..4f461f5f4a --- /dev/null +++ b/test/snapshots/subagent_hooks/should_apply_subagent_lifecycle_hook_outputs.yaml @@ -0,0 +1,420 @@ +# Lifecycle output variant of the shared subagent capture; model responses are unchanged. +models: + - claude-sonnet-5 +conversations: + - messages: + - role: system + content: ${system} + - role: user + content: Use the task tool to spawn an explore agent that reads the file subagent-test.txt in the current directory and + reports its contents. You must use the task tool. + - role: assistant + content: I'll spawn an explore agent to read the file and report its contents. + - role: assistant + tool_calls: + - id: toolcall_0 + type: function + function: + name: report_intent + arguments: '{"intent":"Spawning explore agent"}' + - role: assistant + tool_calls: + - id: toolcall_1 + type: function + function: + name: task + arguments: '{"agent_type":"explore","name":"read-file","description":"Reading subagent-test.txt","prompt":"Read the file + \"subagent-test.txt\" in the current directory (${workdir}) and report its complete contents. Use the + view tool to read the file and provide the full content in your response.","mode":"background"}' + - messages: + - role: system + content: ${system} + - role: user + content: |- + Subagent start hook verified: read the requested file. + + Read the file "subagent-test.txt" in the current directory (${workdir}) and report its complete contents. Use the view tool to read the file and provide the full content in your response. + - role: assistant + content: I'll read the file "subagent-test.txt" from the current directory. + - role: assistant + tool_calls: + - id: toolcall_0 + type: function + function: + name: view + arguments: '{"path":"${workdir}/subagent-test.txt"}' + - messages: + - role: system + content: ${system} + - role: user + content: |- + Subagent start hook verified: read the requested file. + + Read the file "subagent-test.txt" in the current directory (${workdir}) and report its complete contents. Use the view tool to read the file and provide the full content in your response. + - role: assistant + content: I'll read the file "subagent-test.txt" from the current directory. + tool_calls: + - id: toolcall_0 + type: function + function: + name: view + arguments: '{"path":"${workdir}/subagent-test.txt"}' + - role: tool + tool_call_id: toolcall_0 + content: Hello from subagent test! + - role: assistant + content: |- + The complete contents of the file "subagent-test.txt" are: + + ``` + Hello from subagent test! + ``` + + The file contains a single line with the text "Hello from subagent test!". + - messages: + - role: system + content: ${system} + - role: user + content: Use the task tool to spawn an explore agent that reads the file subagent-test.txt in the current directory and + reports its contents. You must use the task tool. + - role: assistant + content: I'll spawn an explore agent to read the file and report its contents. + tool_calls: + - id: toolcall_0 + type: function + function: + name: report_intent + arguments: '{"intent":"Spawning explore agent"}' + - id: toolcall_1 + type: function + function: + name: task + arguments: '{"agent_type":"explore","name":"read-file","description":"Reading subagent-test.txt","prompt":"Read the file + \"subagent-test.txt\" in the current directory (${workdir}) and report its complete contents. Use the + view tool to read the file and provide the full content in your response.","mode":"background"}' + - role: tool + tool_call_id: toolcall_0 + content: Tool 'report_intent' does not exist. + - role: tool + tool_call_id: toolcall_1 + content: "Agent started in background with agent_id: read-file. You'll be notified when it completes. Tell the user + you're waiting and end your response, or continue unrelated work until notified." + - role: assistant + content: I've launched an explore agent to read subagent-test.txt. Waiting for it to complete... + - role: user + content: >- + + + Agent "read-file" (explore) has completed successfully. Use read_agent with agent_id "read-file" to retrieve + the full results. + + + - role: assistant + tool_calls: + - id: toolcall_2 + type: function + function: + name: read_agent + arguments: '{"agent_id":"read-file","wait":true}' + - role: tool + tool_call_id: toolcall_2 + content: >- + Agent completed. agent_id: read-file, agent_type: explore, status: completed, description: Reading + subagent-test.txt, elapsed: 0s, total_turns: 0, duration: 0s + + + Subagent stop hook verified: The complete contents of the file "subagent-test.txt" are: + + + ``` + + Hello from subagent test! + + ``` + + + The file contains a single line with the text "Hello from subagent test!". + - role: assistant + content: |- + The explore agent successfully read the file. The contents of **subagent-test.txt** are: + + ``` + Hello from subagent test! + ``` + # Completion can arrive before the parent reply. With no notification between + # them, the recorded waiting text and read_agent call share one assistant response. + - messages: + - role: system + content: ${system} + - role: user + content: Use the task tool to spawn an explore agent that reads the file subagent-test.txt in the current directory and + reports its contents. You must use the task tool. + - role: assistant + content: I'll spawn an explore agent to read the file and report its contents. + tool_calls: + - id: toolcall_0 + type: function + function: + name: report_intent + arguments: '{"intent":"Spawning explore agent"}' + - id: toolcall_1 + type: function + function: + name: task + arguments: '{"agent_type":"explore","name":"read-file","description":"Reading subagent-test.txt","prompt":"Read the file + \"subagent-test.txt\" in the current directory (${workdir}) and report its complete contents. Use the + view tool to read the file and provide the full content in your response.","mode":"background"}' + - role: tool + tool_call_id: toolcall_0 + content: Tool 'report_intent' does not exist. + - role: tool + tool_call_id: toolcall_1 + content: "Agent started in background with agent_id: read-file. You'll be notified when it completes. Tell the user + you're waiting and end your response, or continue unrelated work until notified." + - role: user + content: >- + + + Agent "read-file" (explore) has completed successfully. Use read_agent with agent_id "read-file" to retrieve + the full results. + + + - role: assistant + content: I've launched an explore agent to read subagent-test.txt. Waiting for it to complete... + tool_calls: + - id: toolcall_2 + type: function + function: + name: read_agent + arguments: '{"agent_id":"read-file","wait":true}' + - role: tool + tool_call_id: toolcall_2 + content: >- + Agent completed. agent_id: read-file, agent_type: explore, status: completed, description: Reading + subagent-test.txt, elapsed: 0s, total_turns: 0, duration: 0s + + + Subagent stop hook verified: The complete contents of the file "subagent-test.txt" are: + + + ``` + + Hello from subagent test! + + ``` + + + The file contains a single line with the text "Hello from subagent test!". + - role: assistant + content: |- + The explore agent successfully read the file. The contents of **subagent-test.txt** are: + + ``` + Hello from subagent test! + ``` + # CLI 1.0.87 can report idle without retrieval advice. This is not a success + # assertion: the unchanged read_agent result below still supplies the contents. + - messages: + - role: system + content: ${system} + - role: user + content: Use the task tool to spawn an explore agent that reads the file subagent-test.txt in the current directory and + reports its contents. You must use the task tool. + - role: assistant + content: I'll spawn an explore agent to read the file and report its contents. + tool_calls: + - id: toolcall_0 + type: function + function: + name: report_intent + arguments: '{"intent":"Spawning explore agent"}' + - id: toolcall_1 + type: function + function: + name: task + arguments: '{"agent_type":"explore","name":"read-file","description":"Reading subagent-test.txt","prompt":"Read the file + \"subagent-test.txt\" in the current directory (${workdir}) and report its complete contents. Use the + view tool to read the file and provide the full content in your response.","mode":"background"}' + - role: tool + tool_call_id: toolcall_0 + content: Tool 'report_intent' does not exist. + - role: tool + tool_call_id: toolcall_1 + content: "Agent started in background with agent_id: read-file. You'll be notified when it completes. Tell the user + you're waiting and end your response, or continue unrelated work until notified." + - role: assistant + content: I've launched an explore agent to read subagent-test.txt. Waiting for it to complete... + - role: user + content: >- + + + Agent "read-file" (explore) has finished processing and is now idle. + + + - role: assistant + tool_calls: + - id: toolcall_2 + type: function + function: + name: read_agent + arguments: '{"agent_id":"read-file","since_turn":0}' + - role: tool + tool_call_id: toolcall_2 + content: >- + Agent completed. agent_id: read-file, agent_type: explore, status: completed, description: Reading + subagent-test.txt, elapsed: 0s, total_turns: 0, duration: 0s + + + Subagent stop hook verified: The complete contents of the file "subagent-test.txt" are: + + + ``` + + Hello from subagent test! + + ``` + + + The file contains a single line with the text "Hello from subagent test!". + - role: assistant + content: |- + The explore agent successfully read the file. The contents of **subagent-test.txt** are: + + ``` + Hello from subagent test! + ``` + - messages: + - role: system + content: ${system} + - role: user + content: Use the task tool to spawn an explore agent that reads the file subagent-test.txt in the current directory and + reports its contents. You must use the task tool. + - role: assistant + content: I'll spawn an explore agent to read the file and report its contents. + tool_calls: + - id: toolcall_0 + type: function + function: + name: report_intent + arguments: '{"intent":"Spawning explore agent"}' + - id: toolcall_1 + type: function + function: + name: task + arguments: '{"agent_type":"explore","name":"read-file","description":"Reading subagent-test.txt","prompt":"Read the file + \"subagent-test.txt\" in the current directory (${workdir}) and report its complete contents. Use the + view tool to read the file and provide the full content in your response.","mode":"background"}' + - role: tool + tool_call_id: toolcall_0 + content: Tool 'report_intent' does not exist. + - role: tool + tool_call_id: toolcall_1 + content: "Agent started in background with agent_id: read-file. You'll be notified when it completes. Tell the user + you're waiting and end your response, or continue unrelated work until notified." + - role: user + content: >- + + + Agent "read-file" (explore) has finished processing and is now idle. + + + - role: assistant + content: I've launched an explore agent to read subagent-test.txt. Waiting for it to complete... + tool_calls: + - id: toolcall_2 + type: function + function: + name: read_agent + arguments: '{"agent_id":"read-file","since_turn":0}' + - role: tool + tool_call_id: toolcall_2 + content: >- + Agent completed. agent_id: read-file, agent_type: explore, status: completed, description: Reading + subagent-test.txt, elapsed: 0s, total_turns: 0, duration: 0s + + + Subagent stop hook verified: The complete contents of the file "subagent-test.txt" are: + + + ``` + + Hello from subagent test! + + ``` + + + The file contains a single line with the text "Hello from subagent test!". + - role: assistant + content: |- + The explore agent successfully read the file. The contents of **subagent-test.txt** are: + + ``` + Hello from subagent test! + ``` + - messages: + - role: system + content: ${system} + - role: user + content: Use the task tool to spawn an explore agent that reads the file subagent-test.txt in the current directory and + reports its contents. You must use the task tool. + - role: assistant + content: I'll spawn an explore agent to read the file and report its contents. + tool_calls: + - id: toolcall_0 + type: function + function: + name: report_intent + arguments: '{"intent":"Spawning explore agent"}' + - id: toolcall_1 + type: function + function: + name: task + arguments: '{"agent_type":"explore","name":"read-file","description":"Reading subagent-test.txt","prompt":"Read the file + \"subagent-test.txt\" in the current directory (${workdir}) and report its complete contents. Use the + view tool to read the file and provide the full content in your response.","mode":"background"}' + - role: tool + tool_call_id: toolcall_0 + content: Tool 'report_intent' does not exist. + - role: tool + tool_call_id: toolcall_1 + content: "Agent started in background with agent_id: read-file. You'll be notified when it completes. Tell the user + you're waiting and end your response, or continue unrelated work until notified." + - role: user + content: >- + + + Agent "read-file" (explore) has finished processing and is now idle. + + + - role: assistant + tool_calls: + - id: toolcall_2 + type: function + function: + name: read_agent + arguments: '{"agent_id":"read-file","since_turn":0}' + - role: tool + tool_call_id: toolcall_2 + content: >- + Agent completed. agent_id: read-file, agent_type: explore, status: completed, description: Reading + subagent-test.txt, elapsed: 0s, total_turns: 0, duration: 0s + + + Subagent stop hook verified: The complete contents of the file "subagent-test.txt" are: + + + ``` + + Hello from subagent test! + + ``` + + + The file contains a single line with the text "Hello from subagent test!". + - role: assistant + content: |- + The explore agent successfully read the file. The contents of **subagent-test.txt** are: + + ``` + Hello from subagent test! + ``` diff --git a/test/snapshots/subagent_hooks/should_invoke_pretooluse_and_posttooluse_hooks_for_sub_agent_tool_calls.yaml b/test/snapshots/subagent_hooks/should_invoke_pretooluse_and_posttooluse_hooks_for_sub_agent_tool_calls.yaml index 7b1c6185c7..529ab700e1 100644 --- a/test/snapshots/subagent_hooks/should_invoke_pretooluse_and_posttooluse_hooks_for_sub_agent_tool_calls.yaml +++ b/test/snapshots/subagent_hooks/should_invoke_pretooluse_and_posttooluse_hooks_for_sub_agent_tool_calls.yaml @@ -338,6 +338,75 @@ conversations: ``` + The file contains a single line with the text "Hello from subagent test!". + - role: assistant + content: |- + The explore agent successfully read the file. The contents of **subagent-test.txt** are: + + ``` + Hello from subagent test! + ``` + # An idle notification can arrive before the parent replies; the parent can + # proceed directly to read_agent without an interim waiting message. + - messages: + - role: system + content: ${system} + - role: user + content: Use the task tool to spawn an explore agent that reads the file subagent-test.txt in the current directory and + reports its contents. You must use the task tool. + - role: assistant + content: I'll spawn an explore agent to read the file and report its contents. + tool_calls: + - id: toolcall_0 + type: function + function: + name: report_intent + arguments: '{"intent":"Spawning explore agent"}' + - id: toolcall_1 + type: function + function: + name: task + arguments: '{"agent_type":"explore","name":"read-file","description":"Reading subagent-test.txt","prompt":"Read the file + \"subagent-test.txt\" in the current directory (${workdir}) and report its complete contents. Use the + view tool to read the file and provide the full content in your response.","mode":"background"}' + - role: tool + tool_call_id: toolcall_0 + content: Tool 'report_intent' does not exist. + - role: tool + tool_call_id: toolcall_1 + content: "Agent started in background with agent_id: read-file. You'll be notified when it completes. Tell the user + you're waiting and end your response, or continue unrelated work until notified." + - role: user + content: >- + + + Agent "read-file" (explore) has finished processing and is now idle. + + + - role: assistant + tool_calls: + - id: toolcall_2 + type: function + function: + name: read_agent + arguments: '{"agent_id":"read-file","since_turn":0}' + - role: tool + tool_call_id: toolcall_2 + content: >- + Agent completed. agent_id: read-file, agent_type: explore, status: completed, description: Reading + subagent-test.txt, elapsed: 0s, total_turns: 0, duration: 0s + + + The complete contents of the file "subagent-test.txt" are: + + + ``` + + Hello from subagent test! + + ``` + + The file contains a single line with the text "Hello from subagent test!". - role: assistant content: |- diff --git a/test/snapshots/telemetry_skills/emits_an_invocation_receipt_without_fabricating_an_execute_tool_span.yaml b/test/snapshots/telemetry_skills/emits_an_invocation_receipt_without_fabricating_an_execute_tool_span.yaml new file mode 100644 index 0000000000..c030383938 --- /dev/null +++ b/test/snapshots/telemetry_skills/emits_an_invocation_receipt_without_fabricating_an_execute_tool_span.yaml @@ -0,0 +1,12 @@ +models: + - claude-sonnet-5 +conversations: + - messages: + - role: system + content: ${system} + - role: user + content: Do not use any tools. Report the verification word from your preloaded review skill and then OTEL_PRELOAD_DONE. + - role: assistant + content: |- + OTEL_SKILL_ORCHID + OTEL_PRELOAD_DONE diff --git a/test/snapshots/telemetry_skills/exports_loader_resource_and_script_facts_capture_false.yaml b/test/snapshots/telemetry_skills/exports_loader_resource_and_script_facts_capture_false.yaml new file mode 100644 index 0000000000..0afe4e55ca --- /dev/null +++ b/test/snapshots/telemetry_skills/exports_loader_resource_and_script_facts_capture_false.yaml @@ -0,0 +1,173 @@ +models: + - claude-sonnet-5 +conversations: + - messages: + - role: system + content: ${system} + - role: user + content: "Use the skill tool to load review. Then use view to read skills/review/references/policy.txt. Then use the + shell tool in sync mode to run exactly: node './skills/review/scripts/check.cjs'. Then use view to read + policy.txt in the working directory, outside the skill. Then use view to read skills/review/SKILL.md, and use + view to list the skills/review directory. After all six calls finish, reply with exactly + OTEL_SKILL_FLOW_DONE." + - role: assistant + tool_calls: + - id: toolcall_0 + type: function + function: + name: skill + arguments: '{"skill":"review"}' + - role: tool + tool_call_id: toolcall_0 + content: Skill "review" loaded successfully. Follow the instructions in the skill context. + - role: user + content: |- + + Base directory for this skill: ${workdir}/skills/review + + Related files (use view tool to read): + - ${workdir}/skills/review/references/policy.txt + - ${workdir}/skills/review/scripts/check.cjs + + Follow the user's explicit instructions. The verification word is OTEL_SKILL_ORCHID. + + + - role: assistant + tool_calls: + - id: toolcall_1 + type: function + function: + name: view + arguments: '{"path":"${workdir}/skills/review/references/policy.txt"}' + - role: tool + tool_call_id: toolcall_1 + content: OTEL_RESOURCE_SAPPHIRE + - role: assistant + tool_calls: + - id: toolcall_2 + type: function + function: + name: ${shell} + arguments: "{\"command\":\"node './skills/review/scripts/check.cjs'\",\"description\":\"Run review skill check + script\"}" + - role: tool + tool_call_id: toolcall_2 + content: |- + OTEL_SCRIPT_AMBER + + - role: assistant + tool_calls: + - id: toolcall_3 + type: function + function: + name: view + arguments: '{"path":"${workdir}/policy.txt"}' + - role: tool + tool_call_id: toolcall_3 + content: OTEL_UNRELATED_POLICY + - role: assistant + tool_calls: + - id: toolcall_4 + type: function + function: + name: view + arguments: '{"path":"${workdir}/skills/review/SKILL.md"}' + - role: assistant + tool_calls: + - id: toolcall_5 + type: function + function: + name: view + arguments: '{"path":"${workdir}/skills/review"}' + - messages: + - role: system + content: ${system} + - role: user + content: "Use the skill tool to load review. Then use view to read skills/review/references/policy.txt. Then use the + shell tool in sync mode to run exactly: node './skills/review/scripts/check.cjs'. Then use view to read + policy.txt in the working directory, outside the skill. Then use view to read skills/review/SKILL.md, and use + view to list the skills/review directory. After all six calls finish, reply with exactly + OTEL_SKILL_FLOW_DONE." + - role: assistant + tool_calls: + - id: toolcall_0 + type: function + function: + name: skill + arguments: '{"skill":"review"}' + - role: tool + tool_call_id: toolcall_0 + content: Skill "review" loaded successfully. Follow the instructions in the skill context. + - role: user + content: |- + + Base directory for this skill: ${workdir}/skills/review + + Related files (use view tool to read): + - ${workdir}/skills/review/references/policy.txt + - ${workdir}/skills/review/scripts/check.cjs + + Follow the user's explicit instructions. The verification word is OTEL_SKILL_ORCHID. + + + - role: assistant + tool_calls: + - id: toolcall_1 + type: function + function: + name: view + arguments: '{"path":"${workdir}/skills/review/references/policy.txt"}' + - role: tool + tool_call_id: toolcall_1 + content: OTEL_RESOURCE_SAPPHIRE + - role: assistant + tool_calls: + - id: toolcall_2 + type: function + function: + name: ${shell} + arguments: "{\"command\":\"node './skills/review/scripts/check.cjs'\",\"description\":\"Run review skill check + script\"}" + - role: tool + tool_call_id: toolcall_2 + content: |- + OTEL_SCRIPT_AMBER + + - role: assistant + tool_calls: + - id: toolcall_3 + type: function + function: + name: view + arguments: '{"path":"${workdir}/policy.txt"}' + - role: tool + tool_call_id: toolcall_3 + content: OTEL_UNRELATED_POLICY + - role: assistant + tool_calls: + - id: toolcall_4 + type: function + function: + name: view + arguments: '{"path":"${workdir}/skills/review/SKILL.md"}' + - id: toolcall_5 + type: function + function: + name: view + arguments: '{"path":"${workdir}/skills/review"}' + - role: tool + tool_call_id: toolcall_4 + content: |- + --- + name: review + description: OTel skill description sentinel + --- + Follow the user's explicit instructions. The verification word is OTEL_SKILL_ORCHID. + - role: tool + tool_call_id: toolcall_5 + content: |- + SKILL.md + references + scripts + - role: assistant + content: OTEL_SKILL_FLOW_DONE diff --git a/test/snapshots/telemetry_skills/exports_loader_resource_and_script_facts_capture_true.yaml b/test/snapshots/telemetry_skills/exports_loader_resource_and_script_facts_capture_true.yaml new file mode 100644 index 0000000000..a9d6021449 --- /dev/null +++ b/test/snapshots/telemetry_skills/exports_loader_resource_and_script_facts_capture_true.yaml @@ -0,0 +1,166 @@ +models: + - claude-sonnet-5 +conversations: + - messages: + - role: system + content: ${system} + - role: user + content: "Use the skill tool to load review. Then use view to read skills/review/references/policy.txt. Then use the + shell tool in sync mode to run exactly: node './skills/review/scripts/check.cjs'. Then use view to read + policy.txt in the working directory, outside the skill. Then use view to read skills/review/SKILL.md, and use + view to list the skills/review directory. After all six calls finish, reply with exactly + OTEL_SKILL_FLOW_DONE." + - role: assistant + tool_calls: + - id: toolcall_0 + type: function + function: + name: skill + arguments: '{"skill":"review"}' + - role: tool + tool_call_id: toolcall_0 + content: Skill "review" loaded successfully. Follow the instructions in the skill context. + - role: user + content: |- + + Base directory for this skill: ${workdir}/skills/review + + Related files (use view tool to read): + - ${workdir}/skills/review/references/policy.txt + - ${workdir}/skills/review/scripts/check.cjs + + Follow the user's explicit instructions. The verification word is OTEL_SKILL_ORCHID. + + + - role: assistant + tool_calls: + - id: toolcall_1 + type: function + function: + name: view + arguments: '{"path":"${workdir}/skills/review/references/policy.txt"}' + - role: tool + tool_call_id: toolcall_1 + content: OTEL_RESOURCE_SAPPHIRE + - role: assistant + tool_calls: + - id: toolcall_2 + type: function + function: + name: ${shell} + arguments: "{\"command\":\"node './skills/review/scripts/check.cjs'\",\"description\":\"Run skill check script\"}" + - role: tool + tool_call_id: toolcall_2 + content: |- + OTEL_SCRIPT_AMBER + + - role: assistant + tool_calls: + - id: toolcall_3 + type: function + function: + name: view + arguments: '{"path":"${workdir}/policy.txt"}' + - role: assistant + tool_calls: + - id: toolcall_4 + type: function + function: + name: view + arguments: '{"path":"${workdir}/skills/review/SKILL.md"}' + - role: assistant + tool_calls: + - id: toolcall_5 + type: function + function: + name: view + arguments: '{"path":"${workdir}/skills/review"}' + - messages: + - role: system + content: ${system} + - role: user + content: "Use the skill tool to load review. Then use view to read skills/review/references/policy.txt. Then use the + shell tool in sync mode to run exactly: node './skills/review/scripts/check.cjs'. Then use view to read + policy.txt in the working directory, outside the skill. Then use view to read skills/review/SKILL.md, and use + view to list the skills/review directory. After all six calls finish, reply with exactly + OTEL_SKILL_FLOW_DONE." + - role: assistant + tool_calls: + - id: toolcall_0 + type: function + function: + name: skill + arguments: '{"skill":"review"}' + - role: tool + tool_call_id: toolcall_0 + content: Skill "review" loaded successfully. Follow the instructions in the skill context. + - role: user + content: |- + + Base directory for this skill: ${workdir}/skills/review + + Related files (use view tool to read): + - ${workdir}/skills/review/references/policy.txt + - ${workdir}/skills/review/scripts/check.cjs + + Follow the user's explicit instructions. The verification word is OTEL_SKILL_ORCHID. + + + - role: assistant + tool_calls: + - id: toolcall_1 + type: function + function: + name: view + arguments: '{"path":"${workdir}/skills/review/references/policy.txt"}' + - role: tool + tool_call_id: toolcall_1 + content: OTEL_RESOURCE_SAPPHIRE + - role: assistant + tool_calls: + - id: toolcall_2 + type: function + function: + name: ${shell} + arguments: "{\"command\":\"node './skills/review/scripts/check.cjs'\",\"description\":\"Run skill check script\"}" + - role: tool + tool_call_id: toolcall_2 + content: |- + OTEL_SCRIPT_AMBER + + - role: assistant + tool_calls: + - id: toolcall_3 + type: function + function: + name: view + arguments: '{"path":"${workdir}/policy.txt"}' + - id: toolcall_4 + type: function + function: + name: view + arguments: '{"path":"${workdir}/skills/review/SKILL.md"}' + - id: toolcall_5 + type: function + function: + name: view + arguments: '{"path":"${workdir}/skills/review"}' + - role: tool + tool_call_id: toolcall_3 + content: OTEL_UNRELATED_POLICY + - role: tool + tool_call_id: toolcall_4 + content: |- + --- + name: review + description: OTel skill description sentinel + --- + Follow the user's explicit instructions. The verification word is OTEL_SKILL_ORCHID. + - role: tool + tool_call_id: toolcall_5 + content: |- + SKILL.md + references + scripts + - role: assistant + content: OTEL_SKILL_FLOW_DONE diff --git a/test/snapshots/telemetry_skills/exports_unknown_skill_and_ordinary_zero_and_nonzero_exits_capture_false.yaml b/test/snapshots/telemetry_skills/exports_unknown_skill_and_ordinary_zero_and_nonzero_exits_capture_false.yaml new file mode 100644 index 0000000000..3eea73411e --- /dev/null +++ b/test/snapshots/telemetry_skills/exports_unknown_skill_and_ordinary_zero_and_nonzero_exits_capture_false.yaml @@ -0,0 +1,43 @@ +models: + - claude-sonnet-5 +conversations: + - messages: + - role: system + content: ${system} + - role: user + content: "Use the skill tool to attempt to load otel-nonexistent. Do not search for or install it. Then use the shell + tool in sync mode to run exactly: node './exit.cjs' 0; exit 0. Then use the shell tool in sync mode to run + exactly: node './exit.cjs' 7; exit 7. A nonzero exit is intentional. Do not retry it. Reply with exactly + OTEL_EXIT_FLOW_DONE." + - role: assistant + tool_calls: + - id: toolcall_0 + type: function + function: + name: skill + arguments: '{"skill":"otel-nonexistent"}' + - role: tool + tool_call_id: toolcall_0 + content: 'Skill "otel-nonexistent" not found. Available skills: review' + - role: assistant + tool_calls: + - id: toolcall_1 + type: function + function: + name: ${shell} + arguments: "{\"command\":\"node './exit.cjs' 0; exit 0\",\"description\":\"Run exit.cjs with 0\"}" + - role: tool + tool_call_id: toolcall_1 + content: + - role: assistant + tool_calls: + - id: toolcall_2 + type: function + function: + name: ${shell} + arguments: "{\"command\":\"node './exit.cjs' 7; exit 7\",\"description\":\"Run exit.cjs with 7\"}" + - role: tool + tool_call_id: toolcall_2 + content: + - role: assistant + content: OTEL_EXIT_FLOW_DONE diff --git a/test/snapshots/telemetry_skills/exports_unknown_skill_and_ordinary_zero_and_nonzero_exits_capture_true.yaml b/test/snapshots/telemetry_skills/exports_unknown_skill_and_ordinary_zero_and_nonzero_exits_capture_true.yaml new file mode 100644 index 0000000000..c2d982e9ad --- /dev/null +++ b/test/snapshots/telemetry_skills/exports_unknown_skill_and_ordinary_zero_and_nonzero_exits_capture_true.yaml @@ -0,0 +1,43 @@ +models: + - claude-sonnet-5 +conversations: + - messages: + - role: system + content: ${system} + - role: user + content: "Use the skill tool to attempt to load otel-nonexistent. Do not search for or install it. Then use the shell + tool in sync mode to run exactly: node './exit.cjs' 0; exit 0. Then use the shell tool in sync mode to run + exactly: node './exit.cjs' 7; exit 7. A nonzero exit is intentional. Do not retry it. Reply with exactly + OTEL_EXIT_FLOW_DONE." + - role: assistant + tool_calls: + - id: toolcall_0 + type: function + function: + name: skill + arguments: '{"skill":"otel-nonexistent"}' + - role: tool + tool_call_id: toolcall_0 + content: 'Skill "otel-nonexistent" not found. Available skills: review' + - role: assistant + tool_calls: + - id: toolcall_1 + type: function + function: + name: ${shell} + arguments: "{\"command\":\"node './exit.cjs' 0; exit 0\",\"description\":\"Run exit.cjs with code 0\"}" + - role: tool + tool_call_id: toolcall_1 + content: + - role: assistant + tool_calls: + - id: toolcall_2 + type: function + function: + name: ${shell} + arguments: "{\"command\":\"node './exit.cjs' 7; exit 7\",\"description\":\"Run exit.cjs with code 7\"}" + - role: tool + tool_call_id: toolcall_2 + content: + - role: assistant + content: OTEL_EXIT_FLOW_DONE diff --git a/test/snapshots/telemetry_skills/keeps_reloaded_skill_provenance_and_child_attribution_separate_capture_false.yaml b/test/snapshots/telemetry_skills/keeps_reloaded_skill_provenance_and_child_attribution_separate_capture_false.yaml new file mode 100644 index 0000000000..aa4b7e799d --- /dev/null +++ b/test/snapshots/telemetry_skills/keeps_reloaded_skill_provenance_and_child_attribution_separate_capture_false.yaml @@ -0,0 +1,243 @@ +models: + - claude-sonnet-5 +conversations: + - messages: + - role: system + content: ${system} + - role: user + content: Use the skill tool to load review, then reply with exactly OTEL_BEFORE_RELOAD_DONE. + - role: assistant + tool_calls: + - id: toolcall_0 + type: function + function: + name: skill + arguments: '{"skill":"review"}' + - role: tool + tool_call_id: toolcall_0 + content: Skill "review" loaded successfully. Follow the instructions in the skill context. + - role: user + content: |- + + Base directory for this skill: ${workdir}/skills/review + + Related files (use view tool to read): + - ${workdir}/skills/review/references/policy.txt + - ${workdir}/skills/review/scripts/check.cjs + + Follow the user's explicit instructions. The verification word is OTEL_SKILL_ORCHID. + + + - role: assistant + content: OTEL_BEFORE_RELOAD_DONE + - role: user + content: "Use the skill tool to load review again. Then use the task tool with agent_type exactly 'task' and mode 'sync' + to load child-review with the skill tool, use view to read skills/child-review/references/policy.txt, then use + the shell tool in sync mode to run exactly: node './skills/child-review/scripts/check.cjs'. The task agent + should report the resource and script words. After the agent finishes, reply with exactly + OTEL_AFTER_RELOAD_DONE." + - role: assistant + tool_calls: + - id: toolcall_1 + type: function + function: + name: skill + arguments: '{"skill":"review"}' + - role: assistant + tool_calls: + - id: toolcall_2 + type: function + function: + name: task + arguments: "{\"name\":\"child-review-check\",\"agent_type\":\"task\",\"mode\":\"sync\",\"description\":\"Load + child-review skill and run check script\",\"prompt\":\"Use the skill tool to load the skill named + 'child-review'. Then use the view tool to read the file skills/child-review/references/policy.txt + (resolve relative to the repo/work directory). Then use the shell/powershell tool in sync mode to run + exactly: node './skills/child-review/scripts/check.cjs'\\n\\nReport back: (1) the \\\"resource word\\\" + found in policy.txt, and (2) the \\\"script word\\\" output by running the check.cjs script. Be explicit + and clear in your final report about both words.\"}" + - messages: + - role: system + content: ${system} + - role: user + content: >- + Use the skill tool to load the skill named 'child-review'. Then use the view tool to read the file + skills/child-review/references/policy.txt (resolve relative to the repo/work directory). Then use the + shell/powershell tool in sync mode to run exactly: node './skills/child-review/scripts/check.cjs' + + + Report back: (1) the "resource word" found in policy.txt, and (2) the "script word" output by running the + check.cjs script. Be explicit and clear in your final report about both words. + - role: assistant + tool_calls: + - id: toolcall_0 + type: function + function: + name: skill + arguments: '{"skill":"child-review"}' + - role: tool + tool_call_id: toolcall_0 + content: Skill "child-review" loaded successfully. Follow the instructions in the skill context. + - role: user + content: |- + + Base directory for this skill: ${workdir}/skills/child-review + + Related files (use view tool to read): + - ${workdir}/skills/child-review/references/policy.txt + - ${workdir}/skills/child-review/scripts/check.cjs + + Follow the user's explicit instructions. The verification word is OTEL_SKILL_ORCHID. + + + - role: assistant + tool_calls: + - id: toolcall_1 + type: function + function: + name: view + arguments: '{"path":"${workdir}/skills/child-review/references/policy.txt"}' + - role: assistant + tool_calls: + - id: toolcall_2 + type: function + function: + name: ${shell} + arguments: "{\"command\":\"node './skills/child-review/scripts/check.cjs'\",\"description\":\"Run check.cjs script\"}" + - messages: + - role: system + content: ${system} + - role: user + content: >- + Use the skill tool to load the skill named 'child-review'. Then use the view tool to read the file + skills/child-review/references/policy.txt (resolve relative to the repo/work directory). Then use the + shell/powershell tool in sync mode to run exactly: node './skills/child-review/scripts/check.cjs' + + + Report back: (1) the "resource word" found in policy.txt, and (2) the "script word" output by running the + check.cjs script. Be explicit and clear in your final report about both words. + - role: assistant + tool_calls: + - id: toolcall_0 + type: function + function: + name: skill + arguments: '{"skill":"child-review"}' + - role: tool + tool_call_id: toolcall_0 + content: Skill "child-review" loaded successfully. Follow the instructions in the skill context. + - role: user + content: |- + + Base directory for this skill: ${workdir}/skills/child-review + + Related files (use view tool to read): + - ${workdir}/skills/child-review/references/policy.txt + - ${workdir}/skills/child-review/scripts/check.cjs + + Follow the user's explicit instructions. The verification word is OTEL_SKILL_ORCHID. + + + - role: assistant + tool_calls: + - id: toolcall_1 + type: function + function: + name: view + arguments: '{"path":"${workdir}/skills/child-review/references/policy.txt"}' + - id: toolcall_2 + type: function + function: + name: ${shell} + arguments: "{\"command\":\"node './skills/child-review/scripts/check.cjs'\",\"description\":\"Run check.cjs script\"}" + - role: tool + tool_call_id: toolcall_1 + content: OTEL_RESOURCE_SAPPHIRE + - role: tool + tool_call_id: toolcall_2 + content: |- + OTEL_SCRIPT_AMBER + + - role: assistant + content: |- + Here’s the report: + + 1. **Resource word** (from `skills/child-review/references/policy.txt`): **OTEL_RESOURCE_SAPPHIRE** + 2. **Script word** (output of running `node './skills/child-review/scripts/check.cjs'`): **OTEL_SCRIPT_AMBER** + - messages: + - role: system + content: ${system} + - role: user + content: Use the skill tool to load review, then reply with exactly OTEL_BEFORE_RELOAD_DONE. + - role: assistant + tool_calls: + - id: toolcall_0 + type: function + function: + name: skill + arguments: '{"skill":"review"}' + - role: tool + tool_call_id: toolcall_0 + content: Skill "review" loaded successfully. Follow the instructions in the skill context. + - role: user + content: |- + + Base directory for this skill: ${workdir}/skills/review + + Related files (use view tool to read): + - ${workdir}/skills/review/references/policy.txt + - ${workdir}/skills/review/scripts/check.cjs + + Follow the user's explicit instructions. The verification word is OTEL_SKILL_ORCHID. + + + - role: assistant + content: OTEL_BEFORE_RELOAD_DONE + - role: user + content: "Use the skill tool to load review again. Then use the task tool with agent_type exactly 'task' and mode 'sync' + to load child-review with the skill tool, use view to read skills/child-review/references/policy.txt, then use + the shell tool in sync mode to run exactly: node './skills/child-review/scripts/check.cjs'. The task agent + should report the resource and script words. After the agent finishes, reply with exactly + OTEL_AFTER_RELOAD_DONE." + - role: assistant + tool_calls: + - id: toolcall_1 + type: function + function: + name: skill + arguments: '{"skill":"review"}' + - id: toolcall_2 + type: function + function: + name: task + arguments: "{\"name\":\"child-review-check\",\"agent_type\":\"task\",\"mode\":\"sync\",\"description\":\"Load + child-review skill and run check script\",\"prompt\":\"Use the skill tool to load the skill named + 'child-review'. Then use the view tool to read the file skills/child-review/references/policy.txt + (resolve relative to the repo/work directory). Then use the shell/powershell tool in sync mode to run + exactly: node './skills/child-review/scripts/check.cjs'\\n\\nReport back: (1) the \\\"resource word\\\" + found in policy.txt, and (2) the \\\"script word\\\" output by running the check.cjs script. Be explicit + and clear in your final report about both words.\"}" + - role: tool + tool_call_id: toolcall_1 + content: Skill "review" loaded successfully. Follow the instructions in the skill context. + - role: tool + tool_call_id: toolcall_2 + content: |- + Here’s the report: + + 1. **Resource word** (from `skills/child-review/references/policy.txt`): **OTEL_RESOURCE_SAPPHIRE** + 2. **Script word** (output of running `node './skills/child-review/scripts/check.cjs'`): **OTEL_SCRIPT_AMBER** + - role: user + content: |- + + Base directory for this skill: ${workdir}/skills/moved-review + + Related files (use view tool to read): + - ${workdir}/skills/moved-review/references/policy.txt + - ${workdir}/skills/moved-review/scripts/check.cjs + + Follow the user's explicit instructions. + + + - role: assistant + content: OTEL_AFTER_RELOAD_DONE diff --git a/test/snapshots/telemetry_skills/keeps_reloaded_skill_provenance_and_child_attribution_separate_capture_true.yaml b/test/snapshots/telemetry_skills/keeps_reloaded_skill_provenance_and_child_attribution_separate_capture_true.yaml new file mode 100644 index 0000000000..2da0419ed1 --- /dev/null +++ b/test/snapshots/telemetry_skills/keeps_reloaded_skill_provenance_and_child_attribution_separate_capture_true.yaml @@ -0,0 +1,217 @@ +models: + - claude-sonnet-5 +conversations: + - messages: + - role: system + content: ${system} + - role: user + content: Use the skill tool to load review, then reply with exactly OTEL_BEFORE_RELOAD_DONE. + - role: assistant + tool_calls: + - id: toolcall_0 + type: function + function: + name: skill + arguments: '{"skill":"review"}' + - role: tool + tool_call_id: toolcall_0 + content: Skill "review" loaded successfully. Follow the instructions in the skill context. + - role: user + content: |- + + Base directory for this skill: ${workdir}/skills/review + + Related files (use view tool to read): + - ${workdir}/skills/review/references/policy.txt + - ${workdir}/skills/review/scripts/check.cjs + + Follow the user's explicit instructions. The verification word is OTEL_SKILL_ORCHID. + + + - role: assistant + content: OTEL_BEFORE_RELOAD_DONE + - role: user + content: "Use the skill tool to load review again. Then use the task tool with agent_type exactly 'task' and mode 'sync' + to load child-review with the skill tool, use view to read skills/child-review/references/policy.txt, then use + the shell tool in sync mode to run exactly: node './skills/child-review/scripts/check.cjs'. The task agent + should report the resource and script words. After the agent finishes, reply with exactly + OTEL_AFTER_RELOAD_DONE." + - role: assistant + tool_calls: + - id: toolcall_1 + type: function + function: + name: skill + arguments: '{"skill":"review"}' + - role: assistant + tool_calls: + - id: toolcall_2 + type: function + function: + name: task + arguments: "{\"name\":\"child-review-check\",\"agent_type\":\"task\",\"mode\":\"sync\",\"description\":\"Load + child-review skill and run check script\",\"prompt\":\"Use the skill tool to load the skill named + 'child-review'. Then use the view tool to read the file skills/child-review/references/policy.txt + (resolve relative to the current working directory). Then use the shell tool in sync mode to run + exactly: node './skills/child-review/scripts/check.cjs'. Report back: (1) the \\\"resource word\\\" + found in the policy.txt file content, and (2) the word/output produced by running the check.cjs script + (the \\\"script word\\\"). Report both words clearly labeled.\"}" + - messages: + - role: system + content: ${system} + - role: user + content: "Use the skill tool to load the skill named 'child-review'. Then use the view tool to read the file + skills/child-review/references/policy.txt (resolve relative to the current working directory). Then use the + shell tool in sync mode to run exactly: node './skills/child-review/scripts/check.cjs'. Report back: (1) the + \"resource word\" found in the policy.txt file content, and (2) the word/output produced by running the + check.cjs script (the \"script word\"). Report both words clearly labeled." + - role: assistant + tool_calls: + - id: toolcall_0 + type: function + function: + name: skill + arguments: '{"skill":"child-review"}' + - role: assistant + tool_calls: + - id: toolcall_1 + type: function + function: + name: view + arguments: '{"path":"${workdir}/skills/child-review/references/policy.txt"}' + - messages: + - role: system + content: ${system} + - role: user + content: "Use the skill tool to load the skill named 'child-review'. Then use the view tool to read the file + skills/child-review/references/policy.txt (resolve relative to the current working directory). Then use the + shell tool in sync mode to run exactly: node './skills/child-review/scripts/check.cjs'. Report back: (1) the + \"resource word\" found in the policy.txt file content, and (2) the word/output produced by running the + check.cjs script (the \"script word\"). Report both words clearly labeled." + - role: assistant + tool_calls: + - id: toolcall_0 + type: function + function: + name: skill + arguments: '{"skill":"child-review"}' + - id: toolcall_1 + type: function + function: + name: view + arguments: '{"path":"${workdir}/skills/child-review/references/policy.txt"}' + - role: tool + tool_call_id: toolcall_0 + content: Skill "child-review" loaded successfully. Follow the instructions in the skill context. + - role: tool + tool_call_id: toolcall_1 + content: OTEL_RESOURCE_SAPPHIRE + - role: user + content: |- + + Base directory for this skill: ${workdir}/skills/child-review + + Related files (use view tool to read): + - ${workdir}/skills/child-review/references/policy.txt + - ${workdir}/skills/child-review/scripts/check.cjs + + Follow the user's explicit instructions. The verification word is OTEL_SKILL_ORCHID. + + + - role: assistant + tool_calls: + - id: toolcall_2 + type: function + function: + name: ${shell} + arguments: "{\"command\":\"node './skills/child-review/scripts/check.cjs'\",\"description\":\"Run check.cjs script\"}" + - role: tool + tool_call_id: toolcall_2 + content: |- + OTEL_SCRIPT_AMBER + + - role: assistant + content: |- + **Resource word** (from policy.txt): OTEL_RESOURCE_SAPPHIRE + + **Script word** (output of `node './skills/child-review/scripts/check.cjs'`): OTEL_SCRIPT_AMBER + - messages: + - role: system + content: ${system} + - role: user + content: Use the skill tool to load review, then reply with exactly OTEL_BEFORE_RELOAD_DONE. + - role: assistant + tool_calls: + - id: toolcall_0 + type: function + function: + name: skill + arguments: '{"skill":"review"}' + - role: tool + tool_call_id: toolcall_0 + content: Skill "review" loaded successfully. Follow the instructions in the skill context. + - role: user + content: |- + + Base directory for this skill: ${workdir}/skills/review + + Related files (use view tool to read): + - ${workdir}/skills/review/references/policy.txt + - ${workdir}/skills/review/scripts/check.cjs + + Follow the user's explicit instructions. The verification word is OTEL_SKILL_ORCHID. + + + - role: assistant + content: OTEL_BEFORE_RELOAD_DONE + - role: user + content: "Use the skill tool to load review again. Then use the task tool with agent_type exactly 'task' and mode 'sync' + to load child-review with the skill tool, use view to read skills/child-review/references/policy.txt, then use + the shell tool in sync mode to run exactly: node './skills/child-review/scripts/check.cjs'. The task agent + should report the resource and script words. After the agent finishes, reply with exactly + OTEL_AFTER_RELOAD_DONE." + - role: assistant + tool_calls: + - id: toolcall_1 + type: function + function: + name: skill + arguments: '{"skill":"review"}' + - id: toolcall_2 + type: function + function: + name: task + arguments: "{\"name\":\"child-review-check\",\"agent_type\":\"task\",\"mode\":\"sync\",\"description\":\"Load + child-review skill and run check script\",\"prompt\":\"Use the skill tool to load the skill named + 'child-review'. Then use the view tool to read the file skills/child-review/references/policy.txt + (resolve relative to the current working directory). Then use the shell tool in sync mode to run + exactly: node './skills/child-review/scripts/check.cjs'. Report back: (1) the \\\"resource word\\\" + found in the policy.txt file content, and (2) the word/output produced by running the check.cjs script + (the \\\"script word\\\"). Report both words clearly labeled.\"}" + - role: tool + tool_call_id: toolcall_1 + content: Skill "review" loaded successfully. Follow the instructions in the skill context. + - role: tool + tool_call_id: toolcall_2 + content: |- + **Resource word** (from policy.txt): OTEL_RESOURCE_SAPPHIRE + + **Script word** (output of `node './skills/child-review/scripts/check.cjs'`): OTEL_SCRIPT_AMBER + - role: user + content: |- + + Base directory for this skill: ${workdir}/skills/moved-review + + Related files (use view tool to read): + - ${workdir}/skills/moved-review/references/policy.txt + - ${workdir}/skills/moved-review/scripts/check.cjs + + Follow the user's explicit instructions. + + + - role: assistant + content: |- + Resource word: OTEL_RESOURCE_SAPPHIRE + Script word: OTEL_SCRIPT_AMBER + + OTEL_AFTER_RELOAD_DONE diff --git a/test/snapshots/tools/reports_rejected_apply_patch_string_schema_to_the_sdk.yaml b/test/snapshots/tools/reports_rejected_apply_patch_string_schema_to_the_sdk.yaml new file mode 100644 index 0000000000..32b4a4fee2 --- /dev/null +++ b/test/snapshots/tools/reports_rejected_apply_patch_string_schema_to_the_sdk.yaml @@ -0,0 +1,10 @@ +models: + - claude-sonnet-5 +conversations: + - messages: + - role: system + content: ${system} + - role: user + content: Reply with exactly Session ready. + - role: assistant + content: Session ready. diff --git a/test/snapshots/tools/string_schema_apply_patch_override_binds_patch_input.yaml b/test/snapshots/tools/string_schema_apply_patch_override_binds_patch_input.yaml new file mode 100644 index 0000000000..7f137e331a --- /dev/null +++ b/test/snapshots/tools/string_schema_apply_patch_override_binds_patch_input.yaml @@ -0,0 +1,22 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. + +models: + - claude-sonnet-5 +conversations: + - messages: + - role: system + content: ${system} + - role: user + content: Use apply_patch to apply the supplied patch. + - role: assistant + tool_calls: + - id: toolcall_0 + type: function + function: + name: apply_patch + arguments: '{"input":"*** Begin Patch\n*** Add File: override-marker.txt\n+from-native\n*** End Patch"}' + - role: tool + tool_call_id: toolcall_0 + content: HOST_PATCH_HANDLED + - role: assistant + content: Host override completed. diff --git a/test/snapshots/ui_elicitation/automated_sdk_ask_user_callback_settles_without_trusted_human_authority.yaml b/test/snapshots/ui_elicitation/automated_sdk_ask_user_callback_settles_without_trusted_human_authority.yaml new file mode 100644 index 0000000000..2117c52820 --- /dev/null +++ b/test/snapshots/ui_elicitation/automated_sdk_ask_user_callback_settles_without_trusted_human_authority.yaml @@ -0,0 +1,23 @@ +models: + - claude-sonnet-5 +conversations: + - messages: + - role: system + content: ${system} + - role: user + content: Invoke the ask_user tool now. Set message to exactly 'Which repository should I inspect?' and request an object + with one required string field named answer. Do not describe the tool call. After the tool returns, reply with + exactly SDK_AUTOMATED_ASK_USER_DONE. + - role: assistant + tool_calls: + - id: toolcall_0 + type: function + function: + name: ask_user + arguments: '{"message":"Which repository should I + inspect?","requestedSchema":{"properties":{"answer":{"type":"string","title":"Answer"}},"required":["answer"]}}' + - role: tool + tool_call_id: toolcall_0 + content: "User responded: github/st-mcp-poc" + - role: assistant + content: SDK_AUTOMATED_ASK_USER_DONE From d12079020c434b872705dc8ee68b3bc61d2f72bb Mon Sep 17 00:00:00 2001 From: "copilot-cli-release-app[bot]" Date: Mon, 5 Oct 2026 14:13:26 +0000 Subject: [PATCH 2/4] Update SDK snapshot for Copilot CLI 1.0.92-5 --- .github/workflows/sdk-dotnet.yml | 29 +- .github/workflows/sdk-go.yml | 32 +- .github/workflows/sdk-java.yml | 33 +- .github/workflows/sdk-nodejs.yml | 33 +- .github/workflows/sdk-platform.yml | 3 + .github/workflows/sdk-python.yml | 34 +- .github/workflows/sdk.yml | 36 +- CONTRIBUTING.md | 13 +- dotnet/src/Generated/Rpc.cs | 164 +++++++- dotnet/src/Generated/SessionEvents.cs | 2 +- dotnet/src/JsonRpc.cs | 3 +- dotnet/src/Session.cs | 20 +- .../test/Unit/ClientSessionLifetimeTests.cs | 3 +- dotnet/test/Unit/RpcMcpListTests.cs | 43 +++ dotnet/test/Unit/SetToolsTests.cs | 115 ++++++ go/rpc/mcp_list_test.go | 68 ++++ go/rpc/zrpc.go | 121 +++++- go/rpc/zrpc_encoding.go | 23 ++ java/scripts/codegen/java.test.ts | 1 + java/scripts/codegen/java.ts | 31 +- .../generated/rpc/AccountAuthInfo.java | 44 +++ .../copilot/generated/rpc/AuthInfo.java | 1 + .../copilot/generated/rpc/AuthInfoType.java | 2 + .../generated/rpc/AuthLoginAccount.java | 37 ++ .../generated/rpc/AuthLoginResultDto.java | 9 +- .../generated/rpc/AuthLoginResultStatus.java | 4 +- .../generated/rpc/AuthLoginStepCompleted.java | 2 +- .../generated/rpc/McpConfiguredServer.java | 43 +++ .../rpc/McpConfiguredServerState.java | 33 ++ .../generated/rpc/ServerAccountApi.java | 12 +- .../generated/rpc/ServerAgentRegistryApi.java | 2 +- .../generated/rpc/ServerAgentsApi.java | 4 +- .../generated/rpc/ServerCatalogApi.java | 6 +- .../generated/rpc/ServerCommandsApi.java | 2 +- .../generated/rpc/ServerEnvironmentsApi.java | 6 +- .../generated/rpc/ServerExtensionsApi.java | 6 +- .../copilot/generated/rpc/ServerHooksApi.java | 2 +- .../copilot/generated/rpc/ServerHostApi.java | 16 +- .../generated/rpc/ServerInstructionsApi.java | 4 +- .../generated/rpc/ServerLlmInferenceApi.java | 6 +- .../rpc/ServerManagedSettingsApi.java | 14 +- .../copilot/generated/rpc/ServerMcpApi.java | 14 +- .../generated/rpc/ServerMcpConfigApi.java | 14 +- .../rpc/ServerMcpInstallationsApi.java | 8 +- .../generated/rpc/ServerModelsApi.java | 6 +- .../generated/rpc/ServerPluginsApi.java | 14 +- .../rpc/ServerPluginsBuiltinApi.java | 2 +- .../rpc/ServerPluginsMarketplacesApi.java | 12 +- .../copilot/generated/rpc/ServerRpc.java | 6 +- .../generated/rpc/ServerRuntimeApi.java | 2 +- .../generated/rpc/ServerSandboxApi.java | 2 +- .../rpc/ServerSandboxProxyCaApi.java | 10 +- .../generated/rpc/ServerSecretsApi.java | 2 +- .../generated/rpc/ServerSessionFsApi.java | 2 +- .../generated/rpc/ServerSessionsApi.java | 68 ++-- .../generated/rpc/ServerSkillsApi.java | 12 +- .../generated/rpc/ServerSkillsConfigApi.java | 4 +- .../rpc/ServerSkillsInstallationsApi.java | 10 +- .../copilot/generated/rpc/ServerToolsApi.java | 2 +- .../generated/rpc/ServerUserSettingsApi.java | 6 +- .../generated/rpc/SessionAccountsApi.java | 6 +- .../rpc/SessionAccountsLoginApi.java | 6 +- .../generated/rpc/SessionAgentApi.java | 14 +- .../rpc/SessionAutopilotObjectiveApi.java | 2 +- .../generated/rpc/SessionCanvasActionApi.java | 2 +- .../generated/rpc/SessionCanvasApi.java | 8 +- .../rpc/SessionCanvasProviderApi.java | 4 +- .../generated/rpc/SessionCommandsApi.java | 16 +- .../generated/rpc/SessionCompletionsApi.java | 4 +- .../generated/rpc/SessionConnectorsApi.java | 26 +- .../rpc/SessionContentExclusionApi.java | 2 +- .../rpc/SessionCustomizationsApi.java | 2 +- .../generated/rpc/SessionDebugApi.java | 2 +- .../generated/rpc/SessionDiagnosticsApi.java | 4 +- .../generated/rpc/SessionEventLogApi.java | 8 +- .../generated/rpc/SessionExtensionsApi.java | 10 +- .../generated/rpc/SessionFleetApi.java | 2 +- .../generated/rpc/SessionGitHubAuthApi.java | 20 +- .../generated/rpc/SessionHistoryApi.java | 20 +- .../generated/rpc/SessionInstructionsApi.java | 4 +- .../rpc/SessionLimitPredictionApi.java | 4 +- .../copilot/generated/rpc/SessionLspApi.java | 2 +- .../rpc/SessionManagedSettingsApi.java | 2 +- .../copilot/generated/rpc/SessionMcpApi.java | 57 +-- .../generated/rpc/SessionMcpAppsApi.java | 12 +- .../generated/rpc/SessionMcpHeadersApi.java | 2 +- .../rpc/SessionMcpListConfiguredParams.java | 30 ++ .../rpc/SessionMcpListConfiguredResult.java | 31 ++ .../generated/rpc/SessionMcpOauthApi.java | 20 +- .../generated/rpc/SessionMcpPromptsApi.java | 4 +- .../generated/rpc/SessionMcpResourcesApi.java | 6 +- .../generated/rpc/SessionMetadataApi.java | 22 +- .../copilot/generated/rpc/SessionModeApi.java | 4 +- .../generated/rpc/SessionModelApi.java | 16 +- .../copilot/generated/rpc/SessionNameApi.java | 6 +- .../generated/rpc/SessionOptionsApi.java | 2 +- .../generated/rpc/SessionPermissionsApi.java | 20 +- .../rpc/SessionPermissionsFolderTrustApi.java | 4 +- .../rpc/SessionPermissionsLocationsApi.java | 6 +- .../rpc/SessionPermissionsPathsApi.java | 10 +- .../rpc/SessionPermissionsUrlsApi.java | 2 +- .../copilot/generated/rpc/SessionPlanApi.java | 10 +- .../generated/rpc/SessionPluginsApi.java | 16 +- .../rpc/SessionPluginsMarketplacesApi.java | 12 +- .../generated/rpc/SessionProviderApi.java | 10 +- .../generated/rpc/SessionProvidersApi.java | 6 +- .../rpc/SessionProvidersModelsApi.java | 4 +- .../generated/rpc/SessionQueueApi.java | 40 +- .../generated/rpc/SessionRemoteApi.java | 6 +- .../copilot/generated/rpc/SessionRpc.java | 18 +- .../generated/rpc/SessionSandboxApi.java | 6 +- .../generated/rpc/SessionScheduleApi.java | 18 +- .../generated/rpc/SessionSettingsApi.java | 4 +- .../generated/rpc/SessionShellApi.java | 8 +- .../generated/rpc/SessionSkillsApi.java | 12 +- .../generated/rpc/SessionTasksApi.java | 26 +- .../generated/rpc/SessionTelemetryApi.java | 4 +- .../generated/rpc/SessionToolsApi.java | 16 +- .../copilot/generated/rpc/SessionUiApi.java | 26 +- .../generated/rpc/SessionUsageApi.java | 2 +- .../generated/rpc/SessionVisibilityApi.java | 4 +- .../generated/rpc/SessionWorkflowApi.java | 26 +- .../rpc/SessionWorkflowJournalApi.java | 4 +- .../generated/rpc/SessionWorkspacesApi.java | 40 +- .../rpc/GeneratedRpcApiCoverageTest.java | 331 ++++++++-------- .../rpc/GeneratedRpcRecordsCoverageTest.java | 6 - nodejs/package.json | 2 +- nodejs/src/cliVersion.ts | 2 +- nodejs/src/generated/rpc.ts | 126 +++++- nodejs/src/generated/session-events.ts | 2 +- nodejs/test/csharp-codegen.test.ts | 19 + .../test/e2e/disabled_mcp_servers.e2e.test.ts | 194 +++++++++- .../test/e2e/extension_env_access.e2e.test.ts | 3 +- nodejs/test/e2e/harness/sdkTestContext.ts | 28 +- .../e2e/mcp_instruction_recovery.e2e.test.ts | 14 +- nodejs/test/e2e/mcp_oauth.e2e.test.ts | 10 + .../test/e2e/rpc_surface_coverage.e2e.test.ts | 2 +- nodejs/test/e2e/telemetry_http.e2e.test.ts | 257 +++++++++++++ nodejs/test/e2e/workflow.e2e.test.ts | 2 + nodejs/test/mcp-list.test.ts | 49 +++ nodejs/test/python-codegen.test.ts | 17 + nodejs/test/rust-codegen.test.ts | 15 + python/copilot/generated/rpc.py | 360 ++++++++++++++---- python/copilot/generated/session_events.py | 2 +- rust/src/generated/api_types.rs | 166 +++++++- rust/src/generated/rpc.rs | 27 +- rust/src/generated/session_events.rs | 2 +- rust/tests/api_types_test.rs | 68 +++- scripts/codegen/python.ts | 19 +- test/harness/stdio-shutdown-runtime.cjs | 6 +- test/harness/test-mcp-server.mjs | 9 + ...thout_withdrawing_peer_tools_mid_turn.yaml | 43 ++- 152 files changed, 2851 insertions(+), 930 deletions(-) create mode 100644 dotnet/test/Unit/RpcMcpListTests.cs create mode 100644 go/rpc/mcp_list_test.go create mode 100644 java/sdk/src/generated/java/com/github/copilot/generated/rpc/AccountAuthInfo.java create mode 100644 java/sdk/src/generated/java/com/github/copilot/generated/rpc/AuthLoginAccount.java create mode 100644 java/sdk/src/generated/java/com/github/copilot/generated/rpc/McpConfiguredServer.java create mode 100644 java/sdk/src/generated/java/com/github/copilot/generated/rpc/McpConfiguredServerState.java create mode 100644 java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionMcpListConfiguredParams.java create mode 100644 java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionMcpListConfiguredResult.java create mode 100644 nodejs/test/e2e/telemetry_http.e2e.test.ts create mode 100644 nodejs/test/mcp-list.test.ts diff --git a/.github/workflows/sdk-dotnet.yml b/.github/workflows/sdk-dotnet.yml index 4e9671c9a9..48531f4c2a 100644 --- a/.github/workflows/sdk-dotnet.yml +++ b/.github/workflows/sdk-dotnet.yml @@ -83,6 +83,13 @@ jobs: DOTNET_TEST_RESULTS_DIRECTORY: TestResults/inprocess DOTNET_TEST_FILTER: "FullyQualifiedName~GitHub.Copilot.Test.E2E.ClientE2ETests.Should_Start_And_Connect_Over_InProcess_Ffi|FullyQualifiedName~GitHub.Copilot.Test.E2E.SessionE2ETests.Should_Receive_Session_Events|FullyQualifiedName~GitHub.Copilot.Test.E2E.ClientOptionsE2ETests.Should_Use_Configured_GitHub_Host_For_Authentication" run: ../scripts/ci/run-dotnet-tests.sh + - name: Validate documentation examples + if: github.event_name != 'merge_group' + working-directory: ${{ inputs.sdk-home }}/scripts/docs-validation + run: | + npm ci + npm run extract + npm run validate:cs - if: failure() uses: actions/upload-artifact@v7 with: @@ -257,25 +264,3 @@ jobs: path: ${{ inputs.sdk-home }}/dotnet/TestResults/ if-no-files-found: warn retention-days: 7 - - docs-dotnet: - name: "Docs (.NET)" - if: inputs.platform == 'all' || inputs.platform == 'docs' - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v7 - timeout-minutes: 4 - with: - persist-credentials: false - - uses: actions/setup-node@v6 - with: - node-version: 22 - - uses: actions/setup-dotnet@v6 - with: - dotnet-version: "10.0.x" - - run: npm ci - working-directory: ${{ inputs.sdk-home }}/scripts/docs-validation - - run: dotnet restore -p:CopilotSkipCliDownload=true - working-directory: ${{ inputs.sdk-home }}/dotnet - - run: npm run extract && npm run validate:cs - working-directory: ${{ inputs.sdk-home }}/scripts/docs-validation diff --git a/.github/workflows/sdk-go.yml b/.github/workflows/sdk-go.yml index 24ef16a35b..6ca019a1f5 100644 --- a/.github/workflows/sdk-go.yml +++ b/.github/workflows/sdk-go.yml @@ -86,6 +86,17 @@ jobs: run: | go test -v -race -timeout=20m ./internal/e2e -run '^TestInProcessFfiE2E$' go test -v -race -timeout=20m ./internal/e2e -run '^TestClientE2E$/^should_use_configured_github_host_for_authentication$' + - if: github.event_name != 'merge_group' && runner.os == 'Linux' + uses: actions/setup-node@v6 + with: + node-version: 22 + - name: Validate documentation examples + if: github.event_name != 'merge_group' && runner.os == 'Linux' + working-directory: ${{ inputs.sdk-home }}/scripts/docs-validation + run: | + npm ci + npm run extract + npm run validate:go go-musl-x64: name: "Go (Alpine x64)" @@ -138,24 +149,3 @@ jobs: go test -v -race -timeout=20m ./internal/e2e -run '^TestInProcessFfiE2E$' || status=$? go test -v -race -timeout=20m ./internal/e2e -run '^TestClientE2E$/^should_use_configured_github_host_for_authentication$' || status=$? exit "$status" - - docs-go: - name: "Docs (Go)" - if: inputs.platform == 'all' || inputs.platform == 'docs' - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v7 - timeout-minutes: 4 - with: - persist-credentials: false - - uses: actions/setup-node@v6 - with: - node-version: 22 - - uses: actions/setup-go@v6 - with: - go-version: "1.24" - cache-dependency-path: ${{ inputs.sdk-home }}/go/go.sum - - run: npm ci - working-directory: ${{ inputs.sdk-home }}/scripts/docs-validation - - run: npm run extract && npm run validate:go - working-directory: ${{ inputs.sdk-home }}/scripts/docs-validation diff --git a/.github/workflows/sdk-java.yml b/.github/workflows/sdk-java.yml index 408298d88c..752670df1a 100644 --- a/.github/workflows/sdk-java.yml +++ b/.github/workflows/sdk-java.yml @@ -94,6 +94,15 @@ jobs: -Dsurefire.failIfNoSpecifiedTests=false \ -Dit.test=InProcessTransportIT,AuthHostE2ETest \ -Dcopilot.inprocess.cli.path="$COPILOT_CLI_PATH" + - if: github.event_name != 'merge_group' && matrix.test-jdk == '25' + run: ./mvnw install -Dmaven.test.skip=true -Dskip.test.harness=true -Dcopilot.native.skip.download=true + - name: Validate documentation examples + if: github.event_name != 'merge_group' && matrix.test-jdk == '25' + working-directory: ${{ inputs.sdk-home }}/scripts/docs-validation + run: | + npm ci + npm run extract + npm run validate:java - if: failure() uses: actions/upload-artifact@v7 with: @@ -210,27 +219,3 @@ jobs: ${{ inputs.sdk-home }}/java/sdk/target/surefire-reports-isolated/ ${{ inputs.sdk-home }}/java/sdk/target/failsafe-reports/ retention-days: 7 - - docs-java: - name: "Docs (Java)" - if: inputs.platform == 'all' || inputs.platform == 'docs' - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v7 - timeout-minutes: 4 - with: - persist-credentials: false - - uses: actions/setup-node@v6 - with: - node-version: 22 - - uses: actions/setup-java@v5 - with: - distribution: microsoft - java-version: "25" - cache: maven - - run: ./mvnw install -Dmaven.test.skip=true -Dskip.test.harness=true -Dcopilot.native.skip.download=true - working-directory: ${{ inputs.sdk-home }}/java - - run: npm ci - working-directory: ${{ inputs.sdk-home }}/scripts/docs-validation - - run: npm run extract && npm run validate:java - working-directory: ${{ inputs.sdk-home }}/scripts/docs-validation diff --git a/.github/workflows/sdk-nodejs.yml b/.github/workflows/sdk-nodejs.yml index 315331609b..1c311b3924 100644 --- a/.github/workflows/sdk-nodejs.yml +++ b/.github/workflows/sdk-nodejs.yml @@ -19,6 +19,11 @@ on: sdk-home: required: true type: string + outputs: + capi-result: + description: "Standard-platform CAPI job result, independent of BYOK jobs" + # Direct result outputs can be empty: https://github.com/actions/runner/issues/2495 + value: ${{ fromJSON(toJSON(jobs.nodejs)).result }} permissions: contents: read @@ -109,6 +114,13 @@ jobs: COPILOT_HMAC_KEY: ${{ secrets.COPILOT_DEVELOPER_CLI_INTEGRATION_HMAC_KEY }} COPILOT_SDK_DEFAULT_CONNECTION: inprocess run: npm test -- test/e2e/inprocess_ffi.e2e.test.ts test/e2e/auth_host.e2e.test.ts + - name: Validate documentation examples + if: github.event_name != 'merge_group' && runner.os == 'Linux' + working-directory: ${{ inputs.sdk-home }}/scripts/docs-validation + run: | + npm ci + npm run extract + npm run validate:ts # Keep each BYOK backend on its own runner; shared runtime E2E coverage belongs # in TypeScript rather than repeating these sweeps across SDK languages. @@ -197,24 +209,3 @@ jobs: npm test || status=$? COPILOT_SDK_DEFAULT_CONNECTION=inprocess npm test -- test/e2e/inprocess_ffi.e2e.test.ts test/e2e/auth_host.e2e.test.ts || status=$? exit "$status" - - docs-typescript: - name: "Docs (TypeScript)" - if: inputs.platform == 'all' || inputs.platform == 'docs' - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v7 - timeout-minutes: 4 - with: - persist-credentials: false - - uses: actions/setup-node@v6 - with: - node-version: 22 - cache: npm - cache-dependency-path: ${{ inputs.sdk-home }}/nodejs/package-lock.json - - run: npm ci --ignore-scripts - working-directory: ${{ inputs.sdk-home }}/nodejs - - run: npm ci - working-directory: ${{ inputs.sdk-home }}/scripts/docs-validation - - run: npm run extract && npm run validate:ts - working-directory: ${{ inputs.sdk-home }}/scripts/docs-validation diff --git a/.github/workflows/sdk-platform.yml b/.github/workflows/sdk-platform.yml index b21a9106fc..f1ed4e6ed4 100644 --- a/.github/workflows/sdk-platform.yml +++ b/.github/workflows/sdk-platform.yml @@ -16,6 +16,9 @@ on: description: "Node.js SDK result, independent of other languages" # Direct result outputs can be empty: https://github.com/actions/runner/issues/2495 value: ${{ fromJSON(toJSON(jobs.nodejs)).result }} + nodejs-capi-result: + description: "Standard-platform Node.js CAPI result, independent of BYOK and other languages" + value: ${{ jobs.nodejs.outputs.capi-result }} permissions: contents: read diff --git a/.github/workflows/sdk-python.yml b/.github/workflows/sdk-python.yml index 71bedf2eaa..66ef2fadca 100644 --- a/.github/workflows/sdk-python.yml +++ b/.github/workflows/sdk-python.yml @@ -83,6 +83,14 @@ jobs: COPILOT_HMAC_KEY: ${{ secrets.COPILOT_DEVELOPER_CLI_INTEGRATION_HMAC_KEY }} COPILOT_SDK_DEFAULT_CONNECTION: inprocess run: uv run --locked pytest -v -s e2e/test_inprocess_ffi_e2e.py e2e/test_auth_host_e2e.py + - name: Validate documentation examples + if: github.event_name != 'merge_group' && runner.os == 'Linux' + working-directory: ${{ inputs.sdk-home }}/scripts/docs-validation + run: | + npm ci + npm run extract + uv run --locked --python 3.12 --project "$GITHUB_WORKSPACE/$SDK_HOME/python" python3 -m mypy --version + uv run --locked --python 3.12 --project "$GITHUB_WORKSPACE/$SDK_HOME/python" npm run validate:py - name: Upload Python test diagnostics if: failure() uses: actions/upload-artifact@v7 @@ -149,29 +157,3 @@ jobs: include-hidden-files: true if-no-files-found: warn retention-days: 7 - - docs-python: - name: "Docs (Python)" - if: inputs.platform == 'all' || inputs.platform == 'docs' - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v7 - timeout-minutes: 4 - with: - persist-credentials: false - - uses: actions/setup-node@v6 - with: - node-version: 22 - - uses: actions/setup-python@v6 - with: - python-version: "3.12" - - uses: astral-sh/setup-uv@v7 - - run: uv sync --locked - working-directory: ${{ inputs.sdk-home }}/python - - run: npm ci - working-directory: ${{ inputs.sdk-home }}/scripts/docs-validation - - run: npm run extract - working-directory: ${{ inputs.sdk-home }}/scripts/docs-validation - - run: uv run --locked --project "$GITHUB_WORKSPACE/$SDK_HOME/python" python3 -m mypy --version - - run: uv run --locked --project "$GITHUB_WORKSPACE/$SDK_HOME/python" npm run validate:py - working-directory: ${{ inputs.sdk-home }}/scripts/docs-validation diff --git a/.github/workflows/sdk.yml b/.github/workflows/sdk.yml index b6314e62f6..979c24425e 100644 --- a/.github/workflows/sdk.yml +++ b/.github/workflows/sdk.yml @@ -317,40 +317,22 @@ jobs: sdk-home: ${{ needs.detect-layout.outputs.sdk-home }} secrets: inherit - sdk-docs: - name: "Documentation" - needs: detect-layout - if: github.event_name != 'merge_group' - uses: ./.github/workflows/sdk-platform.yml - with: - platform: docs - sdk-home: ${{ needs.detect-layout.outputs.sdk-home }} - secrets: inherit - + # Only Linux CAPI gates this rollup; the full SDK aggregate still reports all coverage. sdk-typescript: name: sdk-typescript if: always() - needs: [sdk-linux-x64, sdk-linuxmusl-x64, sdk-darwin-arm64, sdk-win32-x64] + needs: [sdk-linux-x64] runs-on: ubuntu-slim steps: - - name: Check Node.js SDK results + - name: Check Linux CAPI Node.js SDK result env: - EVENT_NAME: ${{ github.event_name }} RESULTS: ${{ toJSON(needs) }} run: | - for job in sdk-linux-x64 sdk-linuxmusl-x64 sdk-darwin-arm64 sdk-win32-x64; do - expected=success - result=$(jq -r --arg job "$job" '.[$job].outputs["nodejs-result"]' <<< "$RESULTS") - # TEMPORARY MERGE QUEUE REDUCTION: only Linux x64 runs in merge groups. - if [[ "$EVENT_NAME" == "merge_group" && "$job" != "sdk-linux-x64" ]]; then - expected=skipped - result=$(jq -r --arg job "$job" '.[$job].result' <<< "$RESULTS") - fi - if [[ "$result" != "$expected" ]]; then - echo "::error::Node.js SDK job $job: expected $expected, got $result" - exit 1 - fi - done + result=$(jq -r '.["sdk-linux-x64"].outputs["nodejs-capi-result"]' <<< "$RESULTS") + if [[ "$result" != "success" ]]; then + echo "::error::Linux CAPI Node.js SDK job: expected success, got $result" + exit 1 + fi check-freshness: name: "Check schema and SDK freshness" @@ -443,7 +425,6 @@ jobs: - sdk-linuxmusl-x64 - sdk-darwin-arm64 - sdk-win32-x64 - - sdk-docs - check-freshness runs-on: ubuntu-slim steps: @@ -470,7 +451,6 @@ jobs: sdk-linuxmusl-x64 sdk-darwin-arm64 sdk-win32-x64 - sdk-docs ) for job in "${required_jobs[@]}"; do if [[ "$(jq -r --arg job "$job" '.[$job].result' <<< "$RESULTS")" != "success" ]]; then diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 9e64a0bb04..ebf27c92a8 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -316,7 +316,12 @@ additional feature/acquisition choices documented in [rust/AGENTS.md](rust/AGENT ### Documentation checks -API snippet validation is separate from SDK tests. From the SDK root: +[SDK CI](.github/workflows/sdk.yml) validates Node.js, Python, Go, .NET, and Java +API snippets after successful tests in their standard Linux jobs (JDK 25 for Java). +These checks run on pull requests, pushes to `main`, and manual workflow runs, +but not merge groups. A documentation failure fails the corresponding language job. + +To validate snippets without running SDK tests, run these commands from the SDK root: ```bash npm --prefix scripts/docs-validation ci @@ -351,6 +356,12 @@ macOS, Windows, and musl smoke jobs remain separate because they have no matching subprocess job. Merge groups retain the reduced Linux TypeScript CAPI subprocess coverage. +The `sdk-typescript` required rollup checks only the Linux CAPI job, including +its build, packaging, and applicable static checks. Other platforms, BYOK +backends, and languages keep their existing scheduling and failure reporting; +they do not gate this rollup. The full `SDK` aggregate still requires all +scheduled coverage to succeed. + The three BYOK backend sweeps run in separate Linux TypeScript jobs, alongside the normal CAPI job; they do not repeat unit tests, packaging, or static checks. After preparing the runtime as described above, run a sweep from the diff --git a/dotnet/src/Generated/Rpc.cs b/dotnet/src/Generated/Rpc.cs index 080abf0116..6dcca141a8 100644 --- a/dotnet/src/Generated/Rpc.cs +++ b/dotnet/src/Generated/Rpc.cs @@ -1309,6 +1309,7 @@ internal sealed class AccountGetQuotaRequest [JsonDerivedType(typeof(AuthInfoTokenProvider), "token-provider")] [JsonDerivedType(typeof(AuthInfoCopilotApiToken), "copilot-api-token")] [JsonDerivedType(typeof(AuthInfoUser), "user")] +[JsonDerivedType(typeof(AuthInfoAccount), "account")] [JsonDerivedType(typeof(AuthInfoGhCli), "gh-cli")] [JsonDerivedType(typeof(AuthInfoApiKey), "api-key")] public partial class AuthInfo @@ -1805,6 +1806,24 @@ public partial class AuthInfoUser : AuthInfo public required string Login { get; set; } } +/// An interactive account whose model provider owns its credentials. It carries no GitHub credential. +/// The account variant of . +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public partial class AuthInfoAccount : AuthInfo +{ + /// + [JsonIgnore] + public override string Type => "account"; + + /// Host coordinate owned by the account's model provider. + [JsonPropertyName("host")] + public required string Host { get; set; } + + /// Login identifying the provider-owned account. + [JsonPropertyName("login")] + public required string Login { get; set; } +} + /// Authentication-info input variant for GitHub CLI credentials, carrying host, login, and the `gh auth token` value. /// The gh-cli variant of . [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] @@ -10298,6 +10317,7 @@ public sealed class SessionSetCredentialsResult [JsonDerivedType(typeof(SettableAuthInfoToken), "token")] [JsonDerivedType(typeof(SettableAuthInfoCopilotApiToken), "copilot-api-token")] [JsonDerivedType(typeof(SettableAuthInfoUser), "user")] +[JsonDerivedType(typeof(SettableAuthInfoAccount), "account")] [JsonDerivedType(typeof(SettableAuthInfoGhCli), "gh-cli")] [JsonDerivedType(typeof(SettableAuthInfoApiKey), "api-key")] public partial class SettableAuthInfo @@ -10428,6 +10448,24 @@ public partial class SettableAuthInfoUser : SettableAuthInfo public required string Login { get; set; } } +/// An interactive account whose model provider owns its credentials. It carries no GitHub credential. +/// The account variant of . +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public partial class SettableAuthInfoAccount : SettableAuthInfo +{ + /// + [JsonIgnore] + public override string Type => "account"; + + /// Host coordinate owned by the account's model provider. + [JsonPropertyName("host")] + public required string Host { get; set; } + + /// Login identifying the provider-owned account. + [JsonPropertyName("login")] + public required string Login { get; set; } +} + /// Authentication-info input variant for GitHub CLI credentials, carrying host, login, and the `gh auth token` value. /// The gh-cli variant of . [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] @@ -11073,10 +11111,35 @@ public partial class AuthLoginStepNeedsInteraction : AuthLoginStep public override string Kind => "needs-interaction"; } -/// Terminal result of an interactive login flow. +/// A credential-free account choice after sign-in. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class AuthLoginAccount +{ + /// Host coordinate owned by the selected account's provider. + [JsonPropertyName("host")] + public string Host { get; set; } = string.Empty; + + /// Provider kind that owns this account choice. + [JsonPropertyName("kind")] + public AccountKind Kind { get; set; } + + /// Human-readable login for the account choice. + [JsonPropertyName("login")] + public string Login { get; set; } = string.Empty; + + /// Opaque identifier supplied to the next login step to select this account. + [JsonPropertyName("selectionId")] + public string SelectionId { get; set; } = string.Empty; +} + +/// Result of an interactive login flow. Pending consent or account selection is not terminal. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] public sealed class AuthLoginResultDto { + /// Available accounts when sign-in is awaiting account selection, ordered with Microsoft 365 first. + [JsonPropertyName("accounts")] + public IList? Accounts { get; set; } + /// Host that was signed in, when completed. [Url] [StringSyntax(StringSyntaxAttribute.Uri)] @@ -11087,7 +11150,7 @@ public sealed class AuthLoginResultDto [JsonPropertyName("login")] public string? Login { get; set; } - /// Terminal disposition of the login. + /// Current disposition of the login, including pending user decisions. [JsonPropertyName("status")] public AuthLoginResultStatus Status { get; set; } } @@ -11100,7 +11163,7 @@ public partial class AuthLoginStepCompleted : AuthLoginStep [JsonIgnore] public override string Kind => "completed"; - /// The terminal login result. + /// Login result. When status is needs-plaintext-consent or needs-account-selection, advance with the user's decision to continue. [JsonPropertyName("result")] public required AuthLoginResultDto Result { get; set; } } @@ -15615,6 +15678,73 @@ internal sealed class SessionMcpListRequest public string SessionId { get; set; } = string.Empty; } +/// Observational state for a matching already materialized MCP server. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class McpConfiguredServerState +{ + /// Observed connection error, when the materialized server failed. + [JsonPropertyName("error")] + public string? Error { get; set; } + + /// Observed connection status. This is not a configuration or readiness guarantee. + [JsonPropertyName("status")] + public McpServerStatus Status { get; set; } +} + +/// Effective MCP configuration entry. Configuration enablement is distinct from the optional live observation. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class McpConfiguredServer +{ + /// Human-readable display name supplied by configuration. + [JsonPropertyName("displayName")] + public string? DisplayName { get; set; } + + /// Whether this configured server is enabled after session configuration and policy filtering. + [JsonPropertyName("enabled")] + public bool Enabled { get; set; } + + /// Observed state from an already materialized matching server. Omitted when no live graph has this configured server; it never determines configuration enablement. + [JsonPropertyName("live")] + public McpConfiguredServerState? Live { get; set; } + + /// Server name (config key). + [RegularExpression("^[^\\x00-\\x1f/\\x7f-\\x9f}]+(?:\\/[^\\x00-\\x1f/\\x7f-\\x9f}]+)*$")] + [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "Safe for generated string properties: JSON Schema minLength/maxLength map to string length validation, not reflection over trimmed Count members")] + [MinLength(1)] + [JsonPropertyName("name")] + public string Name { get; set; } = string.Empty; + + /// Configuration provenance: user, workspace, plugin, builtin, or managed. + [JsonPropertyName("source")] + public McpServerSource? Source { get; set; } + + /// Plugin name that provided this server, when source is plugin. + [JsonPropertyName("sourcePlugin")] + public string? SourcePlugin { get; set; } + + /// Plugin version that provided this server, when source is plugin. + [JsonPropertyName("sourcePluginVersion")] + public string? SourcePluginVersion { get; set; } +} + +/// Effective MCP configuration with optional live observations from matching already materialized servers. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class McpConfiguredServerList +{ + /// Effective configured MCP servers. + [JsonPropertyName("servers")] + public IList Servers { get => field ??= []; set; } +} + +/// Identifies the target session. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +internal sealed class SessionMcpListConfiguredRequest +{ + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; +} + /// Normalized MCP Apps discovery metadata from a tool's `_meta.ui` block. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] public sealed class McpToolUi @@ -34246,6 +34376,9 @@ public AuthInfoType(string value) /// Authentication from an interactive user sign-in. public static AuthInfoType User { get; } = new("user"); + /// Authentication from a selected provider-owned account, without a GitHub credential. + public static AuthInfoType Account { get; } = new("account"); + /// Authentication delegated to the GitHub CLI. public static AuthInfoType GhCli { get; } = new("gh-cli"); @@ -34436,7 +34569,7 @@ public override void Write(Utf8JsonWriter writer, LoginProviderKind value, JsonS } -/// Terminal disposition of a login persistence attempt. +/// Disposition of a login attempt, including pending user decisions. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] @@ -34456,12 +34589,15 @@ public AuthLoginResultStatus(string value) /// Gets the value associated with this . public string Value => _value ?? string.Empty; - /// The credential was persisted and the account is signed in. + /// The credential was persisted and the selected account is signed in. public static AuthLoginResultStatus Completed { get; } = new("completed"); /// Persistence needs explicit consent to store the token in plaintext. public static AuthLoginResultStatus NeedsPlaintextConsent { get; } = new("needs-plaintext-consent"); + /// Credentials are saved; select an account using a returned selectionId as advance input to complete sign-in. + public static AuthLoginResultStatus NeedsAccountSelection { get; } = new("needs-account-selection"); + /// The user declined plaintext persistence. public static AuthLoginResultStatus Declined { get; } = new("declined"); @@ -46402,7 +46538,7 @@ internal McpApi(CopilotSession session) _session = session; } - /// Lists MCP servers configured for the session, their connection status, and host-level state. The host-level state (disabled/filtered servers, failed/needs-auth/pending connections, mcp3p policy, full config) is empty/zero when no MCP host has been initialized for the session. + /// Lists materialized MCP servers and their connection status. Cache misses may start and wait for MCP servers. /// The to monitor for cancellation requests. The default is . /// MCP servers configured for the session, with their connection status and host-level state. public async Task ListAsync(CancellationToken cancellationToken = default) @@ -46413,6 +46549,17 @@ public async Task ListAsync(CancellationToken cancellationToken = return await CopilotClient.InvokeRpcAsync(_session.Rpc, "session.mcp.list", [request], cancellationToken); } + /// Lists effective MCP configuration without starting, restarting, authenticating, or waiting for servers. An optional live observation is from an already materialized matching server; this is not a readiness guarantee. + /// The to monitor for cancellation requests. The default is . + /// Effective MCP configuration with optional live observations from matching already materialized servers. + public async Task ListConfiguredAsync(CancellationToken cancellationToken = default) + { + _session.ThrowIfDisposed(); + + var request = new SessionMcpListConfiguredRequest { SessionId = _session.SessionId }; + return await CopilotClient.InvokeRpcAsync(_session.Rpc, "session.mcp.listConfigured", [request], cancellationToken); + } + /// Lists the tools exposed by a connected MCP server on this session's host. This performs a live `tools/list` request. Tool UI metadata is returned independently of whether MCP Apps rendering is enabled for the session. /// Name of the connected MCP server whose tools to list. /// The to monitor for cancellation requests. The default is . @@ -50543,6 +50690,7 @@ public static void RegisterClientGlobalApiHandlers(JsonRpc rpc, ClientGlobalApiH [JsonSerializable(typeof(AuthIdentity))] [JsonSerializable(typeof(AuthIdentityMetadata))] [JsonSerializable(typeof(AuthInfo))] +[JsonSerializable(typeof(AuthLoginAccount))] [JsonSerializable(typeof(AuthLoginAdvanceRequest))] [JsonSerializable(typeof(AuthLoginBeginRequest))] [JsonSerializable(typeof(AuthLoginBegun))] @@ -50832,6 +50980,9 @@ public static void RegisterClientGlobalApiHandlers(JsonRpc rpc, ClientGlobalApiH [JsonSerializable(typeof(McpConfigUpdateRequest))] [JsonSerializable(typeof(McpConfigureGitHubRequest))] [JsonSerializable(typeof(McpConfigureGitHubResult))] +[JsonSerializable(typeof(McpConfiguredServer))] +[JsonSerializable(typeof(McpConfiguredServerList))] +[JsonSerializable(typeof(McpConfiguredServerState))] [JsonSerializable(typeof(McpDiagnosticDetails))] [JsonSerializable(typeof(McpDiagnosticSourceConfiguration))] [JsonSerializable(typeof(McpDisableRequest))] @@ -51312,6 +51463,7 @@ public static void RegisterClientGlobalApiHandlers(JsonRpc rpc, ClientGlobalApiH [JsonSerializable(typeof(SessionLoadDeferredRepoHooksResult))] [JsonSerializable(typeof(SessionManagedSettingsGetRequest))] [JsonSerializable(typeof(SessionMcpAppsGetHostContextRequest))] +[JsonSerializable(typeof(SessionMcpListConfiguredRequest))] [JsonSerializable(typeof(SessionMcpListRequest))] [JsonSerializable(typeof(SessionMcpMoveLoadingToBackgroundRequest))] [JsonSerializable(typeof(SessionMcpOauthCancelLoginRequest))] diff --git a/dotnet/src/Generated/SessionEvents.cs b/dotnet/src/Generated/SessionEvents.cs index c482a8a69f..993035e6eb 100644 --- a/dotnet/src/Generated/SessionEvents.cs +++ b/dotnet/src/Generated/SessionEvents.cs @@ -21471,7 +21471,7 @@ public McpServerStatus(string value) /// The server is configured but disabled. public static McpServerStatus Disabled { get; } = new("disabled"); - /// The server was intentionally stopped and can be restarted on demand when policy permits; a server quarantined by restrictive managed policy stays stopped and cannot be restarted until the policy allows it. + /// The server is not running: it may not have started yet, may have been explicitly stopped, or may be quarantined by restrictive managed policy. It can be restarted on demand when policy permits. public static McpServerStatus Stopped { get; } = new("stopped"); /// The server is not configured for this session. diff --git a/dotnet/src/JsonRpc.cs b/dotnet/src/JsonRpc.cs index 26d4fe297a..8d45eb6f4a 100644 --- a/dotnet/src/JsonRpc.cs +++ b/dotnet/src/JsonRpc.cs @@ -1039,11 +1039,12 @@ private sealed class IncomingRequestCancellation : IDisposable public IncomingRequestCancellation(long id, CancellationToken connectionClosedToken) { Id = id; - _combinedSource = CancellationTokenSource.CreateLinkedTokenSource(_requestSource.Token, connectionClosedToken); _registration = _requestSource.Token.Register(static state => { ((TaskCompletionSource)state!).TrySetResult(); }, _requestCancelled); + // Cancellation callbacks run in reverse registration order: propagate to handlers before replying. + _combinedSource = CancellationTokenSource.CreateLinkedTokenSource(_requestSource.Token, connectionClosedToken); } public long Id { get; } diff --git a/dotnet/src/Session.cs b/dotnet/src/Session.cs index f0cdde8999..343bd680da 100644 --- a/dotnet/src/Session.cs +++ b/dotnet/src/Session.cs @@ -693,9 +693,9 @@ private static Dictionary ToJsonElementDictionary(IDictiona /// are the same as the tools supplied when creating or resuming a session. /// /// - /// Tool handlers switch after the runtime accepts the replacement. Tool calls already running finish with the handlers - /// that started them. If the runtime rejects the replacement, the previous handlers remain installed and the exception is - /// propagated. Concurrent calls are applied in order. + /// Tool handlers switch when the runtime's acceptance response arrives, before subsequent tool requests are dispatched. + /// Tool calls already running finish with the handlers that started them. If the runtime rejects the replacement, the + /// previous handlers remain installed and the exception is propagated. Concurrent calls are applied in order. /// /// /// The agent sees the new tools from its next model request, which can fall within a turn in progress. A model request @@ -714,9 +714,8 @@ public async Task SetToolsAsync(ICollection tools, Cancel var wireTools = tools.Select(ToProtocolExternalToolDefinition).ToList(); var handlers = BuildToolHandlerMap(tools); - // Cancelling while an earlier call holds the lock sends nothing. Once this call holds it, the - // request runs to completion even if the caller stops waiting, so an accepted replacement still - // installs its handlers. + // Cancelling before sending the request leaves handlers unchanged. Once sent, the request runs + // to completion even if the caller stops waiting, so an accepted replacement still installs its handlers. var replacement = ReplaceToolsAsync(wireTools, handlers, cancellationToken); try { @@ -741,8 +740,13 @@ private async Task ReplaceToolsAsync( await _setToolsLock.WaitAsync(lockCancellationToken); try { - await Rpc.Tools.SetAsync(wireTools, CancellationToken.None); - Volatile.Write(ref _toolHandlers, handlers); + // SemaphoreSlim can grant a released slot while its cancellation continuation is pending. + lockCancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + var request = new ToolsSetRequest { SessionId = SessionId, Tools = wireTools }; + await CopilotClient.InvokeRpcAsync( + JsonRpc, "session.tools.set", [request], null, CancellationToken.None, + onResponseInline: _ => Volatile.Write(ref _toolHandlers, handlers)); } finally { diff --git a/dotnet/test/Unit/ClientSessionLifetimeTests.cs b/dotnet/test/Unit/ClientSessionLifetimeTests.cs index d78ee57932..80e365d010 100644 --- a/dotnet/test/Unit/ClientSessionLifetimeTests.cs +++ b/dotnet/test/Unit/ClientSessionLifetimeTests.cs @@ -2503,7 +2503,8 @@ public async Task SendAndWaitAsync_Waits_For_Earlier_Idle_Handler() using var subscription = session.On(_ => { entered.TrySetResult(); - handlerFinished = release.Wait(TimeSpan.FromSeconds(5)); + release.Wait(); + handlerFinished = true; }); var pending = session.SendAndWaitAsync(new MessageOptions { Prompt = "hello" }); await WaitForRequestAsync(server, "session.send"); diff --git a/dotnet/test/Unit/RpcMcpListTests.cs b/dotnet/test/Unit/RpcMcpListTests.cs new file mode 100644 index 0000000000..6143a6fbbb --- /dev/null +++ b/dotnet/test/Unit/RpcMcpListTests.cs @@ -0,0 +1,43 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +#if NET8_0_OR_GREATER +using GitHub.Copilot.Rpc; +using Xunit; + +namespace GitHub.Copilot.Test.Unit; + +public sealed partial class ClientSessionLifetimeTests +{ + [Fact] + public async Task Session_Rpc_Mcp_List_And_ListConfigured_Use_Parameterless_Wire_Contracts() + { + await using var server = await FakeCopilotServer.StartAsync(); + server.ResponseFactory = _ => new Dictionary { ["servers"] = Array.Empty() }; + await using var client = new CopilotClient(new CopilotClientOptions + { + Connection = RuntimeConnection.ForUri(server.Url) + }); + await using var session = await client.CreateSessionAsync(new SessionConfig()); + server.ClearRequests(); + + var mcp = session.Rpc.Mcp; + Func> legacyList = mcp.ListAsync; + Func> configuredList = mcp.ListConfiguredAsync; + Assert.NotNull(mcp.GetType().GetMethod("ListAsync", [typeof(CancellationToken)])); + Assert.NotNull(mcp.GetType().GetMethod("ListConfiguredAsync", [typeof(CancellationToken)])); + await mcp.ListAsync(); + await legacyList(CancellationToken.None); + await mcp.ListConfiguredAsync(); + await configuredList(CancellationToken.None); + + Assert.Collection( + server.Requests, + request => AssertConnectorRequest(request, "session.mcp.list", session.SessionId), + request => AssertConnectorRequest(request, "session.mcp.list", session.SessionId), + request => AssertConnectorRequest(request, "session.mcp.listConfigured", session.SessionId), + request => AssertConnectorRequest(request, "session.mcp.listConfigured", session.SessionId)); + } +} +#endif diff --git a/dotnet/test/Unit/SetToolsTests.cs b/dotnet/test/Unit/SetToolsTests.cs index 4779a3a70e..bed92108c8 100644 --- a/dotnet/test/Unit/SetToolsTests.cs +++ b/dotnet/test/Unit/SetToolsTests.cs @@ -6,6 +6,7 @@ #pragma warning disable GHCP001 // Live tool replacement is intentionally experimental. using Microsoft.Extensions.AI; +using Microsoft.Extensions.Logging; using System.Net; using System.Net.Sockets; using System.Reflection; @@ -240,6 +241,40 @@ await Assert.ThrowsAnyAsync(() => Assert.Equal("new", response.Params.GetProperty("result").GetProperty("textResultForLlm").GetString()); } + [Fact] + public async Task SetToolsAsync_Publishes_Accepted_Replacement_Before_Next_Tool_Request() + { + await using var server = await SetToolsFakeServer.StartAsync(); + var logger = new SetToolsResponseLogger(); + var releaseSet = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + server.BeforeSetToolsResponseAsync = _ => releaseSet.Task; + await using var client = new CopilotClient(new CopilotClientOptions { Connection = RuntimeConnection.ForUri(server.Url), Logger = logger }); + await using var session = await client.CreateSessionAsync(new SessionConfig + { + Tools = [Tool("cancel_tool", "old")], + }); + + using var cts = new CancellationTokenSource(); + try + { + var replacement = session.SetToolsAsync([Tool("cancel_tool", "new")], cts.Token); + await server.WaitForRequestAsync("session.tools.set"); + await cts.CancelAsync(); + await Assert.ThrowsAnyAsync(() => replacement); + + releaseSet.SetResult(); + await logger.ResponseReceived.Task.WaitAsync(TimeSpan.FromSeconds(5)); + + var result = await InvokeToolAsync(server, session, "cancel_tool", "cancel-request"); + Assert.Equal("new", result); + } + finally + { + releaseSet.TrySetResult(); + logger.ContinueResponse.TrySetResult(); + } + } + [Fact] public async Task SetToolsAsync_Cancellation_While_Queued_Does_Not_Send_Request() { @@ -274,6 +309,52 @@ await Assert.ThrowsAnyAsync(() => Assert.Equal("first", result); } + [Fact] + public async Task SetToolsAsync_Cancellation_Before_Queued_Waiter_Resumes_Does_Not_Send_Request() + { + await using var server = await SetToolsFakeServer.StartAsync(); + var releaseSet = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + server.BeforeSetToolsResponseAsync = _ => releaseSet.Task; + await using var client = new CopilotClient(new CopilotClientOptions { Connection = RuntimeConnection.ForUri(server.Url) }); + await using var session = await client.CreateSessionAsync(new SessionConfig()); + + var first = session.SetToolsAsync([Tool("queued_tool", "first")]); + await server.WaitForRequestAsync("session.tools.set"); + using var cts = new CancellationTokenSource(); + var context = new PausedContinuationContext(); + var previousContext = SynchronizationContext.Current; + Task queued; + try + { + SynchronizationContext.SetSynchronizationContext(context); + queued = session.SetToolsAsync([Tool("queued_tool", "queued")], cts.Token); + } + finally + { + SynchronizationContext.SetSynchronizationContext(previousContext); + releaseSet.TrySetResult(); + } + + await first; + var continuation = await context.Continuation.Task.WaitAsync(TimeSpan.FromSeconds(5)); + try + { + await cts.CancelAsync(); + await Assert.ThrowsAnyAsync(() => queued); + } + finally + { + continuation.Callback(continuation.State); + } + + // A later completed replacement proves the cancelled waiter has left the queue. + await session.SetToolsAsync([Tool("queued_tool", "last")]); + Assert.Equal( + ["Returns first", "Returns last"], + server.Requests.Where(request => request.Method == "session.tools.set") + .Select(request => request.Params.GetProperty("tools")[0].GetProperty("description").GetString())); + } + private static AIFunction Tool(string name, string result) { return AIFunctionFactory.Create( @@ -441,6 +522,40 @@ public void Dispose() } } + private sealed class PausedContinuationContext : SynchronizationContext + { + public TaskCompletionSource<(SendOrPostCallback Callback, object? State)> Continuation { get; } = + new(TaskCreationOptions.RunContinuationsAsynchronously); + + public override void Post(SendOrPostCallback callback, object? state) + { + if (!Continuation.TrySetResult((callback, state))) + { + base.Post(callback, state); + } + } + } + + private sealed class SetToolsResponseLogger : ILogger + { + public TaskCompletionSource ResponseReceived { get; } = new(TaskCreationOptions.RunContinuationsAsynchronously); + public TaskCompletionSource ContinueResponse { get; } = new(TaskCreationOptions.RunContinuationsAsynchronously); + public IDisposable? BeginScope(TState state) where TState : notnull => null; + public bool IsEnabled(LogLevel logLevel) => true; + + public void Log(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func formatter) + { + if (state is IReadOnlyList> properties && + properties.Any(entry => entry is { Key: "Method", Value: "session.tools.set" }) && + properties.Any(entry => entry is { Key: "Status", Value: "Succeeded" })) + { + // Hold the RPC awaiter after acceptance without blocking the reader's next tool event. + ResponseReceived.TrySetResult(); + ContinueResponse.Task.GetAwaiter().GetResult(); + } + } + } + private sealed class SetToolsFakeServer : IAsyncDisposable { private static readonly JsonSerializerOptions s_jsonOptions = new(JsonSerializerDefaults.Web) diff --git a/go/rpc/mcp_list_test.go b/go/rpc/mcp_list_test.go new file mode 100644 index 0000000000..c2e9c47a09 --- /dev/null +++ b/go/rpc/mcp_list_test.go @@ -0,0 +1,68 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. + +package rpc + +import ( + "context" + "encoding/json" + "io" + "testing" + + "github.com/github/copilot-sdk/go/internal/jsonrpc2" +) + +var _ interface { + List(context.Context) (*MCPServerList, error) + ListConfigured(context.Context) (*MCPConfiguredServerList, error) +} = (*MCPAPI)(nil) + +func TestMCPListUsesParameterlessWireContract(t *testing.T) { + clientToServerReader, clientToServerWriter := io.Pipe() + serverToClientReader, serverToClientWriter := io.Pipe() + client := jsonrpc2.NewClient(clientToServerWriter, serverToClientReader) + server := jsonrpc2.NewClient(serverToClientWriter, clientToServerReader) + requests := make(chan map[string]any, 2) + server.SetRequestHandler("session.mcp.list", func(params json.RawMessage) (json.RawMessage, *jsonrpc2.Error) { + var request map[string]any + if err := json.Unmarshal(params, &request); err != nil { + return nil, &jsonrpc2.Error{Code: -32602, Message: err.Error()} + } + requests <- request + return json.RawMessage(`{"servers":[]}`), nil + }) + server.SetRequestHandler("session.mcp.listConfigured", func(params json.RawMessage) (json.RawMessage, *jsonrpc2.Error) { + var request map[string]any + if err := json.Unmarshal(params, &request); err != nil { + return nil, &jsonrpc2.Error{Code: -32602, Message: err.Error()} + } + requests <- request + return json.RawMessage(`{"servers":[]}`), nil + }) + client.Start() + server.Start() + t.Cleanup(func() { + client.Stop() + server.Stop() + _ = clientToServerWriter.Close() + _ = clientToServerReader.Close() + _ = serverToClientWriter.Close() + _ = serverToClientReader.Close() + }) + + mcp := NewSessionRPC(client, "session-1").MCP + if _, err := mcp.List(t.Context()); err != nil { + t.Fatal(err) + } + if _, err := mcp.ListConfigured(t.Context()); err != nil { + t.Fatal(err) + } + got := make([]map[string]any, 2) + for i := range got { + got[i] = <-requests + } + for _, request := range got { + if request["sessionId"] != "session-1" || len(request) != 1 { + t.Fatalf("request = %#v, want only the bound session ID", request) + } + } +} diff --git a/go/rpc/zrpc.go b/go/rpc/zrpc.go index c0af9e58ab..d463088731 100644 --- a/go/rpc/zrpc.go +++ b/go/rpc/zrpc.go @@ -1034,6 +1034,21 @@ func (r RawAuthInfoData) Type() AuthInfoType { return r.Discriminator } +// An interactive account whose model provider owns its credentials. It carries no GitHub +// credential. +// Experimental: AccountAuthInfo is part of an experimental API and may change or be removed. +type AccountAuthInfo struct { + // Host coordinate owned by the account's model provider. + Host string `json:"host"` + // Login identifying the provider-owned account. + Login string `json:"login"` +} + +func (AccountAuthInfo) authInfo() {} +func (AccountAuthInfo) Type() AuthInfoType { + return AuthInfoTypeAccount +} + // Authentication-info input variant for API-key authentication to a non-GitHub LLM // provider, carrying the secret `apiKey` and host. // Experimental: APIKeyAuthInfo is part of an experimental API and may change or be removed. @@ -1193,6 +1208,20 @@ func (UserAuthInfo) Type() AuthInfoType { return AuthInfoTypeUser } +// A credential-free account choice after sign-in. +// Experimental: AuthLoginAccount is part of an experimental API and may change or be +// removed. +type AuthLoginAccount struct { + // Host coordinate owned by the selected account's provider. + Host string `json:"host"` + // Provider kind that owns this account choice. + Kind AccountKind `json:"kind"` + // Human-readable login for the account choice. + Login string `json:"login"` + // Opaque identifier supplied to the next login step to select this account. + SelectionID string `json:"selectionId"` +} + // Advance an in-flight login flow, optionally fulfilling an input-required step. // Experimental: AuthLoginAdvanceRequest is part of an experimental API and may change or be // removed. @@ -1229,15 +1258,18 @@ type AuthLoginCancelRequest struct { FlowID string `json:"flowId"` } -// Terminal result of an interactive login flow. +// Result of an interactive login flow. Pending consent or account selection is not terminal. // Experimental: AuthLoginResultDto is part of an experimental API and may change or be // removed. type AuthLoginResultDto struct { + // Available accounts when sign-in is awaiting account selection, ordered with Microsoft 365 + // first. + Accounts []AuthLoginAccount `json:"accounts,omitzero"` // Host that was signed in, when completed. Host *string `json:"host,omitempty"` // Login that was signed in, when completed. Login *string `json:"login,omitempty"` - // Terminal disposition of the login. + // Current disposition of the login, including pending user decisions. Status AuthLoginResultStatus `json:"status"` } @@ -1271,7 +1303,8 @@ func (AuthLoginStepAwaiting) Kind() AuthLoginStepKind { } type AuthLoginStepCompleted struct { - // The terminal login result. + // Login result. When status is needs-plaintext-consent or needs-account-selection, advance + // with the user's decision to continue. Result AuthLoginResultDto `json:"result"` } @@ -6787,6 +6820,48 @@ type MCPConfigUpdateRequest struct { type MCPConfigUpdateResult struct { } +// Effective MCP configuration entry. Configuration enablement is distinct from the optional +// live observation. +// Experimental: MCPConfiguredServer is part of an experimental API and may change or be +// removed. +type MCPConfiguredServer struct { + // Human-readable display name supplied by configuration. + DisplayName *string `json:"displayName,omitempty"` + // Whether this configured server is enabled after session configuration and policy + // filtering. + Enabled bool `json:"enabled"` + // Observed state from an already materialized matching server. Omitted when no live graph + // has this configured server; it never determines configuration enablement. + Live *MCPConfiguredServerState `json:"live,omitempty"` + // Server name (config key) + Name string `json:"name"` + // Configuration provenance: user, workspace, plugin, builtin, or managed. + Source *MCPServerSource `json:"source,omitempty"` + // Plugin name that provided this server, when source is plugin. + SourcePlugin *string `json:"sourcePlugin,omitempty"` + // Plugin version that provided this server, when source is plugin. + SourcePluginVersion *string `json:"sourcePluginVersion,omitempty"` +} + +// Effective MCP configuration with optional live observations from matching already +// materialized servers. +// Experimental: MCPConfiguredServerList is part of an experimental API and may change or be +// removed. +type MCPConfiguredServerList struct { + // Effective configured MCP servers. + Servers []MCPConfiguredServer `json:"servers"` +} + +// Observational state for a matching already materialized MCP server. +// Experimental: MCPConfiguredServerState is part of an experimental API and may change or +// be removed. +type MCPConfiguredServerState struct { + // Observed connection error, when the materialized server failed. + Error *string `json:"error,omitempty"` + // Observed connection status. This is not a configuration or readiness guarantee. + Status MCPServerStatus `json:"status"` +} + // Credential-free authentication identity used to configure GitHub MCP. // Experimental: MCPConfigureGitHubRequest is part of an experimental API and may change or // be removed. @@ -17146,6 +17221,10 @@ func (RawSettableAuthInfoData) settableAuthInfo() {} func (r RawSettableAuthInfoData) settableAuthInfoType() SettableAuthInfoType { return r.Discriminator } +func (AccountAuthInfo) settableAuthInfo() {} +func (AccountAuthInfo) settableAuthInfoType() SettableAuthInfoType { + return SettableAuthInfoTypeAccount +} func (APIKeyAuthInfo) settableAuthInfo() {} func (APIKeyAuthInfo) settableAuthInfoType() SettableAuthInfoType { return SettableAuthInfoTypeAPIKey @@ -21636,6 +21715,7 @@ const ( type AuthInfoType string const ( + AuthInfoTypeAccount AuthInfoType = "account" AuthInfoTypeAPIKey AuthInfoType = "api-key" AuthInfoTypeCopilotAPIToken AuthInfoType = "copilot-api-token" AuthInfoTypeEnv AuthInfoType = "env" @@ -21646,16 +21726,19 @@ const ( AuthInfoTypeUser AuthInfoType = "user" ) -// Terminal disposition of a login persistence attempt. +// Disposition of a login attempt, including pending user decisions. // Experimental: AuthLoginResultStatus is part of an experimental API and may change or be // removed. type AuthLoginResultStatus string const ( - // The credential was persisted and the account is signed in. + // The credential was persisted and the selected account is signed in. AuthLoginResultStatusCompleted AuthLoginResultStatus = "completed" // The user declined plaintext persistence. AuthLoginResultStatusDeclined AuthLoginResultStatus = "declined" + // Credentials are saved; select an account using a returned selectionId as advance input to + // complete sign-in. + AuthLoginResultStatusNeedsAccountSelection AuthLoginResultStatus = "needs-account-selection" // Persistence needs explicit consent to store the token in plaintext. AuthLoginResultStatusNeedsPlaintextConsent AuthLoginResultStatus = "needs-plaintext-consent" ) @@ -25572,6 +25655,7 @@ const ( type SettableAuthInfoType string const ( + SettableAuthInfoTypeAccount SettableAuthInfoType = "account" SettableAuthInfoTypeAPIKey SettableAuthInfoType = "api-key" SettableAuthInfoTypeCopilotAPIToken SettableAuthInfoType = "copilot-api-token" SettableAuthInfoTypeEnv SettableAuthInfoType = "env" @@ -31295,10 +31379,8 @@ func (a *MCPAPI) IsServerRunning(ctx context.Context, params *MCPIsServerRunning return &result, nil } -// Lists MCP servers configured for the session, their connection status, and host-level -// state. The host-level state (disabled/filtered servers, failed/needs-auth/pending -// connections, mcp3p policy, full config) is empty/zero when no MCP host has been -// initialized for the session. +// Lists materialized MCP servers and their connection status. Cache misses may start and +// wait for MCP servers. // // RPC method: session.mcp.list. // @@ -31317,6 +31399,27 @@ func (a *MCPAPI) List(ctx context.Context) (*MCPServerList, error) { return &result, nil } +// ListConfigured lists effective MCP configuration without starting, restarting, +// authenticating, or waiting for servers. An optional live observation is from an already +// materialized matching server; this is not a readiness guarantee. +// +// RPC method: session.mcp.listConfigured. +// +// Returns: Effective MCP configuration with optional live observations from matching +// already materialized servers. +func (a *MCPAPI) ListConfigured(ctx context.Context) (*MCPConfiguredServerList, error) { + req := map[string]any{"sessionId": a.sessionID} + raw, err := a.client.Request(ctx, "session.mcp.listConfigured", req) + if err != nil { + return nil, err + } + var result MCPConfiguredServerList + if err := json.Unmarshal(raw, &result); err != nil { + return nil, err + } + return &result, nil +} + // ListTools lists the tools exposed by a connected MCP server on this session's host. This // performs a live `tools/list` request. Tool UI metadata is returned independently of // whether MCP Apps rendering is enabled for the session. diff --git a/go/rpc/zrpc_encoding.go b/go/rpc/zrpc_encoding.go index 739081b803..341094e4ab 100644 --- a/go/rpc/zrpc_encoding.go +++ b/go/rpc/zrpc_encoding.go @@ -21,6 +21,12 @@ func unmarshalAuthInfo(data []byte) (AuthInfo, error) { } switch raw.Type { + case AuthInfoTypeAccount: + var d AccountAuthInfo + if err := json.Unmarshal(data, &d); err != nil { + return nil, err + } + return &d, nil case AuthInfoTypeAPIKey: var d APIKeyAuthInfo if err := json.Unmarshal(data, &d); err != nil { @@ -85,6 +91,17 @@ func (r RawAuthInfoData) MarshalJSON() ([]byte, error) { }) } +func (r AccountAuthInfo) MarshalJSON() ([]byte, error) { + type alias AccountAuthInfo + return json.Marshal(struct { + Type AuthInfoType `json:"type"` + alias + }{ + Type: r.Type(), + alias: alias(r), + }) +} + func (r APIKeyAuthInfo) MarshalJSON() ([]byte, error) { type alias APIKeyAuthInfo return json.Marshal(struct { @@ -8140,6 +8157,12 @@ func unmarshalSettableAuthInfo(data []byte) (SettableAuthInfo, error) { } switch raw.Type { + case SettableAuthInfoTypeAccount: + var d AccountAuthInfo + if err := json.Unmarshal(data, &d); err != nil { + return nil, err + } + return &d, nil case SettableAuthInfoTypeAPIKey: var d APIKeyAuthInfo if err := json.Unmarshal(data, &d); err != nil { diff --git a/java/scripts/codegen/java.test.ts b/java/scripts/codegen/java.test.ts index e489eef017..e31271a946 100644 --- a/java/scripts/codegen/java.test.ts +++ b/java/scripts/codegen/java.test.ts @@ -735,6 +735,7 @@ test("static OAuth config preserves the legacy four-argument constructor", () => test("nullable referenced RPC results retain their object DTO and typed wrapper", async () => { const method = { rpcMethod: "session.accounts.getCurrent", + description: null, stability: "experimental", params: { type: "object", diff --git a/java/scripts/codegen/java.ts b/java/scripts/codegen/java.ts index 076ea61848..0a5f3a7bb6 100644 --- a/java/scripts/codegen/java.ts +++ b/java/scripts/codegen/java.ts @@ -2379,13 +2379,22 @@ export function generateRpcClass( } if (localNestedTypes.size > 0 && lines[lines.length - 1] === "") lines.pop(); - const legacyFieldNames = + const legacyConstructor = className === "McpOauthRequiredStaticClientConfig" - ? ["clientId", "clientSecret", "publicClient", "grantType"] + ? { + fieldNames: ["clientId", "clientSecret", "publicClient", "grantType"], + omittedField: "scope", + description: "Creates a static OAuth client configuration without an explicit scope.", + } : className === "SessionFsSetProviderCapabilities" - ? ["sqlite"] - : undefined; - if (legacyFieldNames) { + ? { + fieldNames: ["sqlite"], + omittedField: "binary", + description: "Creates provider capabilities without binary reads.", + } + : undefined; + if (legacyConstructor) { + const legacyFieldNames = legacyConstructor.fieldNames; const legacyFields = legacyFieldNames.map((fieldName) => { const field = fields.find((candidate) => candidate.javaName === fieldName); if (!field) { @@ -2394,14 +2403,13 @@ export function generateRpcClass( return field; }); const omittedFields = fields.filter((field) => !legacyFieldNames.includes(field.javaName)); - const expectedAddedField = className === "SessionFsSetProviderCapabilities" ? "binary" : "scope"; - if (omittedFields.length !== 1 || omittedFields[0].javaName !== expectedAddedField) { + if (omittedFields.length !== 1 || omittedFields[0].javaName !== legacyConstructor.omittedField) { throw new Error(`Unexpected compatibility fields for ${className}`); } lines.push(``); lines.push(` /**`); - lines.push(` * Creates ${className === "SessionFsSetProviderCapabilities" ? "provider capabilities without binary reads" : "a static OAuth client configuration without an explicit scope"}.`); + lines.push(` * ${legacyConstructor.description}`); lines.push(` *`); for (const field of legacyFields) { const description = (field.description || field.propName) @@ -2424,7 +2432,7 @@ export function generateRpcClass( ? readLegacyParameters(schema, className, { ordered: true }) : undefined; if (recordLegacy) { - if (legacyFieldNames) { + if (legacyConstructor) { throw new Error(`Conflicting compatibility constructors for ${className}`); } const legacyNames = new Set(recordLegacy.legacy); @@ -2942,6 +2950,7 @@ async function generateRpcDataClass( /** A single RPC method node parsed from the schema */ interface RpcMethodNode { rpcMethod: string; + description: string | null; visibility: string; stability: string; deprecated: boolean; @@ -2964,6 +2973,7 @@ function buildNamespaceTree(node: Record): NamespaceTree { if ("rpcMethod" in obj) { tree.methods.set(key, { rpcMethod: String(obj.rpcMethod), + description: typeof obj.description === "string" ? obj.description : null, visibility: String(obj.visibility ?? "public"), stability: String(obj.stability ?? "stable"), deprecated: obj.deprecated === true, @@ -3162,7 +3172,8 @@ export function generateApiMethod( const lines: string[] = []; // Javadoc - const description = (method.params as JSONSchema7 | null)?.description + const description = method.description + ?? (method.params as JSONSchema7 | null)?.description ?? (method.result as JSONSchema7 | null)?.description ?? `Invokes {@code ${method.rpcMethod}}.`; const pushJavadoc = (extraLines: string[] = [], includeSessionIdNote = true): void => { diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/AccountAuthInfo.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/AccountAuthInfo.java new file mode 100644 index 0000000000..b6a93fb206 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/AccountAuthInfo.java @@ -0,0 +1,44 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * An interactive account whose model provider owns its credentials. It carries no GitHub credential. + * + * @since 1.0.0 + */ +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonInclude(JsonInclude.Include.NON_NULL) +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public final class AccountAuthInfo extends AuthInfo { + + @JsonProperty("type") + private final String type = "account"; + + @Override + public String getType() { return type; } + + /** Host coordinate owned by the account's model provider. */ + @JsonProperty("host") + private String host; + + /** Login identifying the provider-owned account. */ + @JsonProperty("login") + private String login; + + public String getHost() { return host; } + public void setHost(String host) { this.host = host; } + + public String getLogin() { return login; } + public void setLogin(String login) { this.login = login; } +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/AuthInfo.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/AuthInfo.java index 5d82a47f85..7b03a6a29a 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/AuthInfo.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/AuthInfo.java @@ -25,6 +25,7 @@ @JsonSubTypes.Type(value = TokenProviderAuthInfo.class, name = "token-provider"), @JsonSubTypes.Type(value = CopilotApiTokenAuthInfo.class, name = "copilot-api-token"), @JsonSubTypes.Type(value = UserAuthInfo.class, name = "user"), + @JsonSubTypes.Type(value = AccountAuthInfo.class, name = "account"), @JsonSubTypes.Type(value = GhCliAuthInfo.class, name = "gh-cli"), @JsonSubTypes.Type(value = ApiKeyAuthInfo.class, name = "api-key") }) diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/AuthInfoType.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/AuthInfoType.java index 5f81c8cf84..1be60446c1 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/AuthInfoType.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/AuthInfoType.java @@ -22,6 +22,8 @@ public enum AuthInfoType { ENV("env"), /** The {@code user} variant. */ USER("user"), + /** The {@code account} variant. */ + ACCOUNT("account"), /** The {@code gh-cli} variant. */ GH_CLI("gh-cli"), /** The {@code api-key} variant. */ diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/AuthLoginAccount.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/AuthLoginAccount.java new file mode 100644 index 0000000000..87405999ed --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/AuthLoginAccount.java @@ -0,0 +1,37 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.github.copilot.CopilotExperimental; +import javax.annotation.processing.Generated; + +/** + * A credential-free account choice after sign-in. + * + * @apiNote This type is experimental and may change in a future version. + * + * @since 1.0.0 + */ +@CopilotExperimental +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record AuthLoginAccount( + /** Opaque identifier supplied to the next login step to select this account. */ + @JsonProperty("selectionId") String selectionId, + /** Host coordinate owned by the selected account's provider. */ + @JsonProperty("host") String host, + /** Human-readable login for the account choice. */ + @JsonProperty("login") String login, + /** Provider kind that owns this account choice. */ + @JsonProperty("kind") AccountKind kind +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/AuthLoginResultDto.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/AuthLoginResultDto.java index cbcffb6e54..ecbb106f12 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/AuthLoginResultDto.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/AuthLoginResultDto.java @@ -11,10 +11,11 @@ import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; import com.github.copilot.CopilotExperimental; +import java.util.List; import javax.annotation.processing.Generated; /** - * Terminal result of an interactive login flow. + * Result of an interactive login flow. Pending consent or account selection is not terminal. * * @apiNote This type is experimental and may change in a future version. * @@ -25,11 +26,13 @@ @JsonInclude(JsonInclude.Include.NON_NULL) @JsonIgnoreProperties(ignoreUnknown = true) public record AuthLoginResultDto( - /** Terminal disposition of the login. */ + /** Current disposition of the login, including pending user decisions. */ @JsonProperty("status") AuthLoginResultStatus status, /** Host that was signed in, when completed. */ @JsonProperty("host") String host, /** Login that was signed in, when completed. */ - @JsonProperty("login") String login + @JsonProperty("login") String login, + /** Available accounts when sign-in is awaiting account selection, ordered with Microsoft 365 first. */ + @JsonProperty("accounts") List accounts ) { } diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/AuthLoginResultStatus.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/AuthLoginResultStatus.java index 9e36ad0027..07becf2fb9 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/AuthLoginResultStatus.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/AuthLoginResultStatus.java @@ -11,7 +11,7 @@ import javax.annotation.processing.Generated; /** - * Terminal disposition of a login persistence attempt. + * Disposition of a login attempt, including pending user decisions. * * @apiNote This type is experimental and may change in a future version. * @@ -24,6 +24,8 @@ public enum AuthLoginResultStatus { COMPLETED("completed"), /** The {@code needs-plaintext-consent} variant. */ NEEDS_PLAINTEXT_CONSENT("needs-plaintext-consent"), + /** The {@code needs-account-selection} variant. */ + NEEDS_ACCOUNT_SELECTION("needs-account-selection"), /** The {@code declined} variant. */ DECLINED("declined"); diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/AuthLoginStepCompleted.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/AuthLoginStepCompleted.java index b1fb524643..a8ea53e123 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/AuthLoginStepCompleted.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/AuthLoginStepCompleted.java @@ -32,7 +32,7 @@ public final class AuthLoginStepCompleted extends AuthLoginStep { @Override public String getKind() { return kind; } - /** The terminal login result. */ + /** Login result. When status is needs-plaintext-consent or needs-account-selection, advance with the user's decision to continue. */ @JsonProperty("result") private AuthLoginResultDto result; diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/McpConfiguredServer.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/McpConfiguredServer.java new file mode 100644 index 0000000000..4e37d915f8 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/McpConfiguredServer.java @@ -0,0 +1,43 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.github.copilot.CopilotExperimental; +import javax.annotation.processing.Generated; + +/** + * Effective MCP configuration entry. Configuration enablement is distinct from the optional live observation. + * + * @apiNote This type is experimental and may change in a future version. + * + * @since 1.0.0 + */ +@CopilotExperimental +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record McpConfiguredServer( + /** Server name (config key) */ + @JsonProperty("name") String name, + /** Whether this configured server is enabled after session configuration and policy filtering. */ + @JsonProperty("enabled") Boolean enabled, + /** Configuration provenance: user, workspace, plugin, builtin, or managed. */ + @JsonProperty("source") McpServerSource source, + /** Plugin name that provided this server, when source is plugin. */ + @JsonProperty("sourcePlugin") String sourcePlugin, + /** Plugin version that provided this server, when source is plugin. */ + @JsonProperty("sourcePluginVersion") String sourcePluginVersion, + /** Human-readable display name supplied by configuration. */ + @JsonProperty("displayName") String displayName, + /** Observed state from an already materialized matching server. Omitted when no live graph has this configured server; it never determines configuration enablement. */ + @JsonProperty("live") McpConfiguredServerState live +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/McpConfiguredServerState.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/McpConfiguredServerState.java new file mode 100644 index 0000000000..0087f2546b --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/McpConfiguredServerState.java @@ -0,0 +1,33 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.github.copilot.CopilotExperimental; +import javax.annotation.processing.Generated; + +/** + * Observational state for a matching already materialized MCP server. + * + * @apiNote This type is experimental and may change in a future version. + * + * @since 1.0.0 + */ +@CopilotExperimental +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record McpConfiguredServerState( + /** Observed connection status. This is not a configuration or readiness guarantee. */ + @JsonProperty("status") McpServerStatus status, + /** Observed connection error, when the materialized server failed. */ + @JsonProperty("error") String error +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerAccountApi.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerAccountApi.java index 0adbf18092..714ef4ebb6 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerAccountApi.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerAccountApi.java @@ -28,7 +28,7 @@ public final class ServerAccountApi { } /** - * Optional opaque account selection or compatibility GitHub token used to look up quota. + * Gets Copilot quota usage for the current or opaquely selected authenticated user. *

* Invokes the method with no params, applying the runtime defaults. * @@ -41,7 +41,7 @@ public CompletableFuture getQuota() { } /** - * Optional opaque account selection or compatibility GitHub token used to look up quota. + * Gets Copilot quota usage for the current or opaquely selected authenticated user. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -52,7 +52,7 @@ public CompletableFuture getQuota(AccountGetQuotaParams p } /** - * Current authentication state + * Gets the currently active authentication credentials from the global auth manager. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -63,7 +63,7 @@ public CompletableFuture getCurrentAuth() { } /** - * List of all authenticated users + * Gets all authenticated users available for account switching. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -74,7 +74,7 @@ public CompletableFuture> getAllUsers() { } /** - * Credentials to validate and store. Omit login to resolve the authenticated user from the token. + * Validates and stores authentication credentials. When login is omitted, resolves the authenticated user from the token before persistence. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -85,7 +85,7 @@ public CompletableFuture login(AccountLoginParams params) { } /** - * User to log out + * Removes user authentication from keychain and persisted state. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerAgentRegistryApi.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerAgentRegistryApi.java index f398fb2dcb..a96ce680d7 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerAgentRegistryApi.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerAgentRegistryApi.java @@ -27,7 +27,7 @@ public final class ServerAgentRegistryApi { } /** - * Inputs to spawn a managed-server child via the controller's spawn delegate. + * Spawns a managed-server child with the supplied configuration and returns a discriminated-union result. The caller (typically the CLI controller) is responsible for attaching to the spawned child and sending any follow-up prompt. When the controller-local spawn gate is closed the server returns JSON-RPC MethodNotFound. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerAgentsApi.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerAgentsApi.java index 0c621a3624..0fd067f72f 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerAgentsApi.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerAgentsApi.java @@ -27,7 +27,7 @@ public final class ServerAgentsApi { } /** - * Optional project paths to include in agent discovery. + * Discovers custom agents across user, project, plugin, and remote sources. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -38,7 +38,7 @@ public CompletableFuture discover(AgentsDiscoverParams par } /** - * Optional project paths to include when enumerating agent discovery directories. + * Returns the canonical directories where a client may create custom agents that the runtime will recognize, including ones that do not exist yet. Project directories become active once created. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerCatalogApi.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerCatalogApi.java index 32e7963e35..f7d7c2fe1b 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerCatalogApi.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerCatalogApi.java @@ -28,7 +28,7 @@ public final class ServerCatalogApi { } /** - * A bounded catalog search. Both the query length and the result count are capped by the schema so a caller cannot request an unbounded scan. + * Requests a bounded catalog search. This host-implemented server method is available through SDK/TUI hosts; standalone and C-ABI runtimes whose host does not implement server-method dispatch return JSON-RPC MethodNotFound. A runtime with search available returns inert candidate summaries, each with an opaque single-use handle scoped to this runtime instance; a runtime without it returns the typed search-unavailable result. Public authorities may be searched anonymously, while an authority that requires credentials yields the typed authentication-required result. All returned text, URLs, and package metadata are untrusted external data and can never trigger instructions, tools, or installation. Read-only: nothing is installed, configured, or persisted. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -39,7 +39,7 @@ public CompletableFuture search(CatalogSearchParams params) } /** - * A bounded catalog search. Both the query length and the result count are capped by the schema so a caller cannot request an unbounded scan. + * Requests a bounded catalog search. This host-implemented server method is available through SDK/TUI hosts; standalone and C-ABI runtimes whose host does not implement server-method dispatch return JSON-RPC MethodNotFound. A runtime with search available returns inert candidate summaries, each with an opaque single-use handle scoped to this runtime instance; a runtime without it returns the typed search-unavailable result. Public authorities may be searched anonymously, while an authority that requires credentials yields the typed authentication-required result. All returned text, URLs, and package metadata are untrusted external data and can never trigger instructions, tools, or installation. Read-only: nothing is installed, configured, or persisted. *

* Accepts the extensible request, including inputs added after the params record. * @@ -52,7 +52,7 @@ public CompletableFuture search(CatalogSearchRequest reques } /** - * Terminates one retained catalog selection group through an opaque reference previously returned by the model-safe search projection. + * Terminates one retained catalog selection group. A selected outcome returns the native host a fresh single-use candidate handle plus the original searchId for a later explicit mcp.planInstall call; non-selected outcomes release the group without producing a planning input. Candidate state, cards, URLs, credentials and private identifiers remain inside the runtime. The model-facing catalog_select tool projects the result separately and never exposes the candidate handle or searchId. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerCommandsApi.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerCommandsApi.java index efa5317ea4..82c5fad8c4 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerCommandsApi.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerCommandsApi.java @@ -27,7 +27,7 @@ public final class ServerCommandsApi { } /** - * Slash commands available in the session, after applying any include/exclude filters. + * Lists the well-known built-in slash commands that work as the first message in a new session (e.g. /plan, /env), without requiring an active session. Commands that depend on session state, authentication, or a synced session are omitted. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerEnvironmentsApi.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerEnvironmentsApi.java index 9d52f2f1ea..a8220ef6fc 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerEnvironmentsApi.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerEnvironmentsApi.java @@ -27,7 +27,7 @@ public final class ServerEnvironmentsApi { } /** - * Optional discovery filters supported by GitHub Mission Control. + * Lists GitHub Mission Control environments visible to the authenticated identity. Does not require a running host and excludes host relay credentials. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -38,7 +38,7 @@ public CompletableFuture list(EnvironmentsListParams par } /** - * Identify a Mission Control environment to retrieve. + * Gets safe discovery information for a GitHub Mission Control environment without requiring a running host. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -49,7 +49,7 @@ public CompletableFuture get(EnvironmentsGetParams params } /** - * Identify a user-managed Mission Control environment to delete. + * Deletes a user-managed GitHub Mission Control environment. GitHub-managed environments cannot be deleted. Does not stop a running host, which may register again. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerExtensionsApi.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerExtensionsApi.java index 7bc74b441f..385fc3bbc2 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerExtensionsApi.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerExtensionsApi.java @@ -27,7 +27,7 @@ public final class ServerExtensionsApi { } /** - * Extensions discovered from persisted Copilot home state and their effective loading mode. Launch-scoped additional plugins are not included. + * Discovers user and enabled installed-plugin extensions from persisted Copilot home state, including enablement preferences. Launch-scoped additional plugins are not included. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -38,7 +38,7 @@ public CompletableFuture discover() { } /** - * Source-qualified extension identifiers to persistently enable for future sessions. + * Persistently enables extension IDs for future sessions. Active sessions are unchanged; use session.extensions.enable to update them. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -49,7 +49,7 @@ public CompletableFuture enable(ExtensionsEnableParams params) { } /** - * Source-qualified extension identifiers to persistently disable for future sessions. + * Persistently disables extension IDs for future sessions. Active sessions are unchanged; use session.extensions.disable to update them. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerHooksApi.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerHooksApi.java index b6f69bf7dc..f41fbd63ac 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerHooksApi.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerHooksApi.java @@ -27,7 +27,7 @@ public final class ServerHooksApi { } /** - * Optional project paths and host-exclusion behavior for server-scoped hook discovery. + * Discovers hook actions enabled under server-side discovery settings from user, repository, plugin, and managed-policy sources. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerHostApi.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerHostApi.java index 2cb0c99c37..a921cdf945 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerHostApi.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerHostApi.java @@ -27,7 +27,7 @@ public final class ServerHostApi { } /** - * Publishes a resident session attached to the listener's owning connection. + * Publishes an attached resident session for this listener's lifetime without copying it. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -38,7 +38,7 @@ public CompletableFuture publishSession(HostPublishSes } /** - * One application-owned session handoff, requested by the supervised hosting participant. + * Requests app-owned materialization over the owning SDK participant. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -49,7 +49,7 @@ CompletableFuture createSession(HostCreateSessionParams } /** - * Ends one participation, not the application's session lifetime. + * Releases app ownership retention after AHP detaches. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -60,7 +60,7 @@ CompletableFuture releaseSession(HostReleaseSessionParams params) { } /** - * Starts a supervised AHP host with at least one explicitly selected transport. + * Starts a connection-owned AHP host with explicit localServer and/or githubEnvironment transports as a supervised SDK participant. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -71,7 +71,7 @@ public CompletableFuture start(HostStartParams params) { } /** - * Stops a connection-owned listener and joins its teardown. + * Stops a listener owned by this SDK connection and joins its cleanup without deleting sessions. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -82,7 +82,7 @@ public CompletableFuture dispose(HostDisposeParams params) { } /** - * Empty acknowledgement for a completed host lifecycle operation. + * Resolves current authenticated credentials and remote-control policy only for the runtime-owned Mission Control hosting participant. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -93,7 +93,7 @@ CompletableFuture getEnvironmentCredentials() { } /** - * Empty acknowledgement for a completed host lifecycle operation. + * Returns listener settings only to the supervised hosting participant over its SDK connection. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -104,7 +104,7 @@ CompletableFuture getConfiguration() { } /** - * Readiness reported by the supervised hosting participant on its own SDK connection. + * Reports a supervised hosting participant's bound AHP endpoint after its SDK handshake. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerInstructionsApi.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerInstructionsApi.java index 70eb5b0211..23d8c7e2de 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerInstructionsApi.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerInstructionsApi.java @@ -27,7 +27,7 @@ public final class ServerInstructionsApi { } /** - * Optional project paths to include in instruction discovery. + * Discovers instruction sources across user, repository, and plugin sources. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -38,7 +38,7 @@ public CompletableFuture discover(InstructionsDiscov } /** - * Optional project paths to include when enumerating instruction discovery targets. + * Returns the canonical files and directories where a client may create custom instructions that the runtime will recognize, including ones that do not exist yet. Repository targets become active once created. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerLlmInferenceApi.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerLlmInferenceApi.java index 4b7663f1f3..753eccb9f3 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerLlmInferenceApi.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerLlmInferenceApi.java @@ -27,7 +27,7 @@ public final class ServerLlmInferenceApi { } /** - * Indicates whether the calling client was registered as the LLM inference provider. + * Registers an SDK client as the LLM inference callback provider. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -38,7 +38,7 @@ public CompletableFuture setProvider() { } /** - * Response head. + * Delivers the response head (status + headers) for an in-flight request, correlated by the requestId the runtime supplied in httpRequestStart. Must be called exactly once per request before any httpResponseChunk frames. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -49,7 +49,7 @@ public CompletableFuture httpResponseStart( } /** - * A response body chunk or terminal error. + * Delivers a body byte range (or a terminal transport error) for an in-flight response, correlated by requestId. Set `end` true on the last chunk. When `error` is set the response terminates with a transport-level failure and the runtime raises an APIConnectionError. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerManagedSettingsApi.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerManagedSettingsApi.java index 4f9cd612bf..a686df708c 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerManagedSettingsApi.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerManagedSettingsApi.java @@ -27,7 +27,7 @@ public final class ServerManagedSettingsApi { } /** - * Validated device-managed settings discovered before a session exists. + * Discovers device-managed settings from production MDM and managed-file sources, validates them against the runtime-owned managed-settings schema, and returns the canonical JSON without requiring a session. `managedSettings.resolve` returns the same device settings together with the account's server policy. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -38,7 +38,7 @@ public CompletableFuture read() { } /** - * Invokes {@code managedSettings.clearCache}. + * Force-refreshes enterprise managed settings for every account: wipes the persistent server-policy cache (the whole `/managed-settings` directory) and drops this runtime process's in-memory retained server policy. It does not itself fetch policy — the effect is that the next time a session resolves managed settings for an account, that resolution re-fetches the account's org policy from the network instead of serving a cached response. Note that `managedSettings.read` returns only device/MDM settings and never triggers the account server-policy fetch, so a host implementing "sync account policy" should call `managedSettings.resolve` or start a fresh session resolution rather than treat a subsequent `managedSettings.read` as the refreshed org policy. Mirrors the invalidation a sign-out performs, broadened from the one signing-out account to all of them; device/MDM layers describe the machine, not the account, and are left untouched. Rejects if the on-disk cache cannot be removed. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -49,7 +49,7 @@ public CompletableFuture clearCache() { } /** - * Optional opaque account selection or GitHub token whose managed settings are resolved. + * Resolves the effective enterprise managed settings without a session, from the device channel and, when an account is available, the account's server policy through the same per-account cache sessions use. A cached server policy less than an hour old is used without a fetch; otherwise the policy is fetched, and when the fetch fails a cached policy up to 24 hours old is used instead, unless `forceRemoteSettingsRefresh` requires a live fetch. With no account requested or signed in, it reports device policy only; signing out removes the account's cached policy. It can fetch server policy over the network when the cache is stale, so call it off latency-critical paths such as startup rather than before listing models. The policy helper is not run. `layers` lists each channel's document before merging, and `values` and `meta` carry typed effective values and their lock state for the keys typed so far. *

* Invokes the method with no params, applying the runtime defaults. * @@ -62,7 +62,7 @@ public CompletableFuture resolve() { } /** - * Optional opaque account selection or GitHub token whose managed settings are resolved. + * Resolves the effective enterprise managed settings without a session, from the device channel and, when an account is available, the account's server policy through the same per-account cache sessions use. A cached server policy less than an hour old is used without a fetch; otherwise the policy is fetched, and when the fetch fails a cached policy up to 24 hours old is used instead, unless `forceRemoteSettingsRefresh` requires a live fetch. With no account requested or signed in, it reports device policy only; signing out removes the account's cached policy. It can fetch server policy over the network when the cache is stale, so call it off latency-critical paths such as startup rather than before listing models. The policy helper is not run. `layers` lists each channel's document before merging, and `values` and `meta` carry typed effective values and their lock state for the keys typed so far. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -73,7 +73,7 @@ public CompletableFuture resolve(ManagedSettingsRe } /** - * The authoring JSON schema for managed settings recognized by this runtime. + * Returns the managed-settings authoring JSON schema with descriptive `x-composition` annotations aligned with the shared settings-engine vocabulary. These annotations are not a complete runtime composition contract: model, effortLevel, and contextTier remain coupled. Use `managedSettings.compose` for the runtime's effective result. Performs no I/O. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -84,7 +84,7 @@ public CompletableFuture schema() { } /** - * A candidate managed-settings document to validate without applying it. + * Validates a candidate managed-settings document the way the runtime validates delivered policy, without applying it. Reports errors that would reject the document, warnings for content the runtime ignores, and the canonical document it would apply. Document text nested more than 64 levels deep is rejected. Performs no I/O. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -95,7 +95,7 @@ public CompletableFuture validate(ManagedSettings } /** - * Candidate managed-settings documents to merge without applying them. + * Merges candidate managed-settings documents for the device, server, and policy-helper channels into the effective settings the runtime would enforce on this host, using the same precedence and composition rules as live resolution, without applying them. Like live resolution, a server's advisory sandbox force-enable is declined on a host that cannot run the sandbox. Does not fetch policy or read policy files, but may perform blocking OS or subprocess probes for sandbox support. Preview documents are limited to 1 MiB and 64 levels of nesting. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerMcpApi.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerMcpApi.java index 9557a2f4ac..553a644214 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerMcpApi.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerMcpApi.java @@ -35,7 +35,7 @@ public final class ServerMcpApi { } /** - * Optional working directory used as context for MCP server discovery. + * Discovers MCP servers from user, workspace, plugin, and builtin sources. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -46,7 +46,7 @@ public CompletableFuture discover(McpDiscoverParams params) { } /** - * A side-effect-free request for an MCP install plan. Computing a plan never writes configuration, stores a secret, or reloads MCP servers. + * Requests a side-effect-free MCP install plan from a catalog candidate handle or a caller-supplied card. This host-implemented server method is available through SDK/TUI hosts; standalone and C-ABI runtimes whose host does not implement server-method dispatch return JSON-RPC MethodNotFound. A runtime with planning available returns a normalised plan and opaque single-use plan handle; a runtime without it returns the typed planning-unavailable result. A completed plan reports resource identity, provenance, eligible transport choices, the user-scope target, required typed values and secret placeholders, the policy result, the configuration changes installing would make, and whether a reload would be needed. Planning never writes configuration, stores a secret, or reloads MCP servers, so abandoning a plan needs no call and leaves nothing behind. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -57,7 +57,7 @@ public CompletableFuture planInstall(McpPlanInstallParams } /** - * A side-effect-free request for an MCP install plan. Computing a plan never writes configuration, stores a secret, or reloads MCP servers. + * Requests a side-effect-free MCP install plan from a catalog candidate handle or a caller-supplied card. This host-implemented server method is available through SDK/TUI hosts; standalone and C-ABI runtimes whose host does not implement server-method dispatch return JSON-RPC MethodNotFound. A runtime with planning available returns a normalised plan and opaque single-use plan handle; a runtime without it returns the typed planning-unavailable result. A completed plan reports resource identity, provenance, eligible transport choices, the user-scope target, required typed values and secret placeholders, the policy result, the configuration changes installing would make, and whether a reload would be needed. Planning never writes configuration, stores a secret, or reloads MCP servers, so abandoning a plan needs no call and leaves nothing behind. *

* Accepts the extensible request, including inputs added after the params record. * @@ -70,7 +70,7 @@ public CompletableFuture planInstall(McpPlanInstallRequest } /** - * Side-effect-free preparation of one original bound remote MCP choice. + * Consumes a bound catalogue plan and retains one exact fully resolved personal remote MCP operation requiring no supplied values or configured secrets. Returns its runtime operation ID and original expiry before any confirmation, activation, writer initialisation or installation effect. Register the original connection, operation and selected-session binding before calling applyInstall. Missing lower owned admission is unavailable, never a raw-config fallback. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -81,7 +81,7 @@ public CompletableFuture prepareInstall(McpPrep } /** - * Applies exactly one previously prepared operation on its original connection. + * Consumes a retained prepared MCP operation once, revalidates its original authority, requests explicit human consent through installations.confirm on the original connection, then revalidates source and applies the sealed transaction. An uncertain result requires original-operation inspection or recovery, never replay. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -92,7 +92,7 @@ public CompletableFuture applyInstall(McpApplyInstallPara } /** - * Read-only preparation of one owned removal under fresh selected-session authority. + * Prepares a read-only removal plan for an exact owned receipt under the selected existing session. Returns the original operation ID before confirmation; neither planning nor abandonment changes configuration or shared OAuth credentials. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -103,7 +103,7 @@ public CompletableFuture planUninstall(McpPlanU } /** - * One-use application of the exact retained removal plan. + * Consumes the original owned-removal plan once and requests fresh exact human confirmation on its original connection. Drift is refused; unrelated manual configuration and shared OAuth credentials are preserved. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerMcpConfigApi.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerMcpConfigApi.java index 6d3510f515..7271e94e29 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerMcpConfigApi.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerMcpConfigApi.java @@ -27,7 +27,7 @@ public final class ServerMcpConfigApi { } /** - * User-configured MCP servers, keyed by server name. + * Lists MCP servers from user configuration. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -38,7 +38,7 @@ public CompletableFuture list() { } /** - * MCP server name and configuration to add to user configuration. + * Adds an MCP server to user configuration. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -49,7 +49,7 @@ public CompletableFuture add(McpConfigAddParams params) { } /** - * MCP server name and replacement configuration to write to user configuration. + * Updates an MCP server in user configuration. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -60,7 +60,7 @@ public CompletableFuture update(McpConfigUpdateParams params) { } /** - * MCP server name to remove from user configuration. + * Removes an MCP server from user configuration. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -71,7 +71,7 @@ public CompletableFuture remove(McpConfigRemoveParams params) { } /** - * MCP server names to enable for new sessions. + * Enables MCP servers in user configuration for new sessions. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -82,7 +82,7 @@ public CompletableFuture enable(McpConfigEnableParams params) { } /** - * MCP server names to disable for new sessions. + * Disables MCP servers in user configuration for new sessions. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -93,7 +93,7 @@ public CompletableFuture disable(McpConfigDisableParams params) { } /** - * Invokes {@code mcp.config.reload}. + * Drops this runtime process's in-memory MCP server-definition cache so the next MCP config read observes disk. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerMcpInstallationsApi.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerMcpInstallationsApi.java index ec99b766e9..6055d0fd3f 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerMcpInstallationsApi.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerMcpInstallationsApi.java @@ -27,7 +27,7 @@ public final class ServerMcpInstallationsApi { } /** - * New-work inventory or recovery request under an explicitly selected existing session. + * Reads receipt-owned MCP inventory for the selected account and host without activating servers or reconstructing missing ownership. Configuration ownership does not prove session-specific usability. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -38,7 +38,7 @@ public CompletableFuture list(McpInstallationsL } /** - * New-work inventory or recovery request under an explicitly selected existing session. + * Reconciles already-confirmed durable MCP transactions, then inspects owned inventory. Does not replay apply or reconstruct deleted ownership metadata; unresolved or unsafe evidence remains an explicit refusal. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -49,7 +49,7 @@ public CompletableFuture recover(McpInstallatio } /** - * Existing-operation control. A new session selector is deliberately not accepted. + * Inspects a known operation only on its original connection. Remains available after account or selected-session loss; does not acquire new authority or rebind an operation. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -60,7 +60,7 @@ public CompletableFuture status(McpInstallation } /** - * Existing-operation control. A new session selector is deliberately not accepted. + * Requests cancellation of a known operation on its original connection, including before apply or confirmation. Already-started effects retain their transaction lease and report an honest terminal or recovery outcome. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerModelsApi.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerModelsApi.java index e4fd3cbc53..491340551d 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerModelsApi.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerModelsApi.java @@ -27,7 +27,7 @@ public final class ServerModelsApi { } /** - * Optional opaque account selection or compatibility GitHub token used to list models. + * Lists Copilot models available to the authenticated user. *

* Invokes the method with no params, applying the runtime defaults. * @@ -40,7 +40,7 @@ public CompletableFuture list() { } /** - * Optional opaque account selection or compatibility GitHub token used to list models. + * Lists Copilot models available to the authenticated user. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -51,7 +51,7 @@ public CompletableFuture list(ModelsListParams params) { } /** - * The running runtime's complete catalog of well-known built-in model IDs, including supported models and additional IDs with built-in metadata. + * Returns the running runtime's complete catalog of well-known built-in model IDs without authentication or network access. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerPluginsApi.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerPluginsApi.java index da5ac6c0e8..4fd1a839b0 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerPluginsApi.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerPluginsApi.java @@ -34,7 +34,7 @@ public final class ServerPluginsApi { } /** - * Plugins installed in user/global state. + * Lists plugins installed in user/global state. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -45,7 +45,7 @@ public CompletableFuture list() { } /** - * Plugin source and optional working directory for relative-path resolution. + * Installs a plugin from a marketplace, GitHub repo, URL, or local path. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -56,7 +56,7 @@ public CompletableFuture install(PluginsInstallParams para } /** - * Name (or spec) of the plugin to uninstall. + * Uninstalls an installed plugin. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -67,7 +67,7 @@ public CompletableFuture uninstall(PluginsUninstallParams params) { } /** - * Name (or spec) of the plugin to update. + * Updates an installed plugin to its latest published version. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -78,7 +78,7 @@ public CompletableFuture update(PluginsUpdateParams params) } /** - * Result of updating all installed plugins. + * Updates every installed plugin to its latest published version. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -89,7 +89,7 @@ public CompletableFuture updateAll() { } /** - * Plugin names (or specs) to enable, plus the optional working directory the repository-controlled guard is evaluated against. + * Enables installed plugins for new sessions. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -100,7 +100,7 @@ public CompletableFuture enable(PluginsEnableParams params) { } /** - * Plugin names (or specs) to disable, plus the optional working directory the repository-controlled guard is evaluated against. + * Disables installed plugins for new sessions. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerPluginsBuiltinApi.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerPluginsBuiltinApi.java index 0610fcb28e..b35a77a817 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerPluginsBuiltinApi.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerPluginsBuiltinApi.java @@ -27,7 +27,7 @@ public final class ServerPluginsBuiltinApi { } /** - * Trusted built-in plugin directories to use for this runtime process. + * Replaces this server's trusted built-in plugin directories while no sessions are active. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerPluginsMarketplacesApi.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerPluginsMarketplacesApi.java index e01bbeeec7..93adce4997 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerPluginsMarketplacesApi.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerPluginsMarketplacesApi.java @@ -27,7 +27,7 @@ public final class ServerPluginsMarketplacesApi { } /** - * All registered marketplaces, including built-in defaults. + * Lists all registered marketplaces (defaults + user-added). * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -38,7 +38,7 @@ public CompletableFuture list() { } /** - * Marketplace source and optional working directory for relative-path resolution. + * Registers a new marketplace from a source (owner/repo, URL, or local path). * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -49,7 +49,7 @@ public CompletableFuture add(PluginsMarketplacesAd } /** - * Name of the marketplace to remove and an optional force flag. + * Removes a previously-registered marketplace. When the marketplace has dependent plugins and `force` is not set, the marketplace is left intact and the result lists the dependents so the caller can decide whether to retry with `force=true`. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -60,7 +60,7 @@ public CompletableFuture remove(PluginsMarketpl } /** - * Name of the marketplace whose plugin catalog to fetch. + * Lists plugins advertised by a registered marketplace. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -71,7 +71,7 @@ public CompletableFuture browse(PluginsMarketpl } /** - * Optional marketplace name; omit to refresh all. + * Re-fetches one or all registered marketplace catalogs. *

* Invokes the method with no params, applying the runtime defaults. * @@ -84,7 +84,7 @@ public CompletableFuture refresh() { } /** - * Optional marketplace name; omit to refresh all. + * Re-fetches one or all registered marketplace catalogs. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerRpc.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerRpc.java index 7eb9aa0118..809b5f2ec3 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerRpc.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerRpc.java @@ -105,7 +105,7 @@ public ServerRpc(RpcCaller caller) { } /** - * Optional message to echo back to the caller. + * Checks server responsiveness and returns protocol information. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -116,7 +116,7 @@ public CompletableFuture ping(PingParams params) { } /** - * Connection-level opt-ins for the `server.connect` handshake. Transport authentication is consumed by the native protocol boundary before dispatch. + * Performs the SDK server connection handshake and validates the optional connection token. Marked internal because this is JSON-RPC transport plumbing invoked automatically by an SDK client's own `connect()` wrapper, not a user-facing method. Stays internal as long as the SDK client owns the handshake; would only become public if the SDK ever exposed the raw schema surface to consumers without a connection wrapper. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -127,7 +127,7 @@ CompletableFuture connect(ConnectParams params) { } /** - * Invokes {@code registerExtensionLaunchProvider}. + * Registers the calling SDK client as the per-entrypoint extension launch provider. Call before creating any sessions. When omitted, the runtime uses its built-in extension launcher. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerRuntimeApi.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerRuntimeApi.java index e57db70946..d09f784d8d 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerRuntimeApi.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerRuntimeApi.java @@ -27,7 +27,7 @@ public final class ServerRuntimeApi { } /** - * Invokes {@code runtime.shutdown}. + * Gracefully shuts down an SDK-owned runtime. The response is sent only after cleanup completes; callers may then terminate the owned runtime process. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerSandboxApi.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerSandboxApi.java index 2634955e4a..1484140f6a 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerSandboxApi.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerSandboxApi.java @@ -31,7 +31,7 @@ public final class ServerSandboxApi { } /** - * Whether the host running this runtime can run the command sandbox. The runtime checks `supported` once per process. A capability answer can change while the process runs, for example after the user installs a missing package. + * Reports whether the host running this runtime can run the command sandbox, without starting a session or spawning a sandboxed command. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerSandboxProxyCaApi.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerSandboxProxyCaApi.java index 0ffe1733d5..0cc59738e9 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerSandboxProxyCaApi.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerSandboxProxyCaApi.java @@ -27,7 +27,7 @@ public final class ServerSandboxProxyCaApi { } /** - * Identifies the credential hosts that the persistent certificate authority of the sandbox credential proxy must cover. The runtime always adds the hosts from the saved user settings. + * Reports whether the persistent certificate authority of the sandbox credential proxy exists, whether OS trust includes it, and whether it must be rotated. Changes nothing. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -38,7 +38,7 @@ public CompletableFuture getStatus(SandboxProxyCa } /** - * Identifies the credential hosts that the persistent certificate authority of the sandbox credential proxy must cover. The runtime always adds the hosts from the saved user settings. + * Creates the persistent certificate authority of the sandbox credential proxy if none is stored, without changing OS trust, and returns the path of its public certificate. Keeps an existing certificate authority, even one that must be rotated. Fails where OS trust is unsupported. Trust it with sandbox.proxyCa.trust: the CLI trusts only the hosts in the saved user settings, so it refuses a certificate authority that also covers hosts from sandboxConfig. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -49,7 +49,7 @@ public CompletableFuture create(SandboxProxyCaCreate } /** - * Identifies the credential hosts that the persistent certificate authority of the sandbox credential proxy must cover. The runtime always adds the hosts from the saved user settings. + * Replaces the persistent certificate authority of the sandbox credential proxy with a new one for the current credential hosts. If OS trust included the old one, removes it and trusts the new one, which can show an OS authentication prompt. Running sandboxed tools keep the old certificate authority until they restart. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -60,7 +60,7 @@ public CompletableFuture rotate(SandboxProxyCaRotate } /** - * Identifies the credential hosts that the persistent certificate authority of the sandbox credential proxy must cover. The runtime always adds the hosts from the saved user settings. + * Adds the persistent certificate authority of the sandbox credential proxy to OS trust, so sandboxed clients that read only OS trust accept the proxy. Call create first. Refuses a certificate authority that is not constrained to the current credential hosts. Can show an OS authentication prompt. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -71,7 +71,7 @@ public CompletableFuture trust(SandboxProxyCaTrustPar } /** - * Status of the persistent certificate authority of the sandbox credential proxy. + * Removes the persistent certificate authority of the sandbox credential proxy from OS trust. Keeps the stored certificate authority. Can show an OS authentication prompt. Sandboxed clients that read only OS trust then reject the proxy; clients that read the per-process certificate bundle continue to work. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerSecretsApi.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerSecretsApi.java index 7f17687818..ebdcac5e9e 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerSecretsApi.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerSecretsApi.java @@ -27,7 +27,7 @@ public final class ServerSecretsApi { } /** - * Secret values to add to the redaction filter. + * Registers secret values for redaction in session logs and exports. The SDK calls this to inject dynamically generated secret values (e.g., OIDC tokens). * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerSessionFsApi.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerSessionFsApi.java index 872702997a..ff48cdfb76 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerSessionFsApi.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerSessionFsApi.java @@ -27,7 +27,7 @@ public final class ServerSessionFsApi { } /** - * Initial working directory, session-state path layout, and path conventions used to register the calling SDK client as the session filesystem provider. A registered provider is authoritative for path interpretation and filesystem facts used by workspace permission validation. Paths are interpreted lexically; home-relative paths (`~` and `~/...`) and Windows drive-relative paths such as `C:foo` are unsupported. Until provider-side canonicalization is supported, providers must not expose symlinks inside allowed roots that escape those roots. + * Registers an SDK client as the session filesystem provider. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerSessionsApi.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerSessionsApi.java index 0b7f099245..eb3546a191 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerSessionsApi.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerSessionsApi.java @@ -28,7 +28,7 @@ public final class ServerSessionsApi { } /** - * Open a session by creating, resuming, attaching, connecting to a remote, or handing off. + * Creates or resumes a local session and returns the opened session ID. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -39,7 +39,7 @@ public CompletableFuture open(SessionsOpenParams params) { } /** - * Source session identifier to fork from, optional event-ID boundary, and optional friendly name for the new session. + * Creates a new session by forking persisted history from an existing session. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -50,7 +50,7 @@ public CompletableFuture fork(SessionsForkParams params) { } /** - * Remote session connection parameters. + * Connects to an existing remote session and exposes it as an SDK session. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -61,7 +61,7 @@ public CompletableFuture connect(SessionsConnectParams pa } /** - * Optional source filter, metadata-load limit, and context filter applied to the returned sessions. + * Lists sessions, optionally filtered by source and working-directory context. Returned entries are discriminated by `isRemote`: local entries carry only the lightweight `LocalSessionMetadataValue` shape; remote entries carry the full `RemoteSessionMetadataValue` shape (repository, PR number, taskType, etc.). *

* Invokes the method with no params, applying the runtime defaults. * @@ -74,7 +74,7 @@ public CompletableFuture list() { } /** - * Optional source filter, metadata-load limit, and context filter applied to the returned sessions. + * Lists sessions, optionally filtered by source and working-directory context. Returned entries are discriminated by `isRemote`: local entries carry only the lightweight `LocalSessionMetadataValue` shape; remote entries carry the full `RemoteSessionMetadataValue` shape (repository, PR number, taskType, etc.). * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -85,7 +85,7 @@ public CompletableFuture list(SessionsListParams params) { } /** - * Session ID whose persisted metadata should be read. + * Reads lightweight persisted metadata for one local session without opening it. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -96,7 +96,7 @@ CompletableFuture getMetadata(SessionsGetMetadataPara } /** - * Bounded batch request for client-owned metadata from persisted local sessions. + * Reads client-owned metadata for multiple persisted local sessions without opening them. Results preserve request order and report missing, corrupt, unsupported, or temporarily unavailable sessions independently. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -107,7 +107,7 @@ public CompletableFuture> getClientMetadata(SessionsGetClientMetada } /** - * Pagination options for reading an inactive or active local session's persisted event journal. + * Reads a page of durable events directly from a local session's persisted journal without creating, resuming, or activating the session. The first read pins the currently opened journal generation and its byte-length boundary; opaque cursor continuations remain on that generation across runtime-owned compaction, truncation, and rewrite operations, which replace the live path atomically, and events appended after the boundary are excluded. For cold hydration, await the first successful page before activation and establish lossless live-event buffering before resume; merge subsequent live events by ID, preserving persisted order and letting live payloads win. Continuations are process-local, single-use capabilities bound to the originating session and storage context and must be paged sequentially; concurrent or repeated use of the same cursor expires that duplicate read rather than reading the generation twice. A complete snapshot has cursorStatus 'ok' and hasMore false. Snapshots expire after five idle minutes, with at most eight retained per process and idle-only eviction under pressure; completion and cancelled-worker exit release their handles. No transcript copy is created, but retained handles may keep replaced files' disk blocks alive until release. Pages have a soft 1 MiB serialized event-array budget including resolved binary assets; one oversized event is returned alone to guarantee progress. Working memory also includes a record/lookahead and asset resolution; resolving the first binary reference may scan the full pinned generation to build a bounded offset index. If the snapshot expires, is evicted, is cancelled before a continuation is established, or becomes unreadable after an observable unsupported in-place shortening, the continuation returns cursorStatus 'expired' with an empty terminal page and never falls back to a different generation. A missing or initially unreadable journal is an RPC error. Persisted history excludes ephemeral events and may omit payloads that are reconstructed only for an active session; use the active session event stream for post-resume live events. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -118,7 +118,7 @@ public CompletableFuture readPersistedEvents( } /** - * Limit for non-empty local session IDs. + * Lists recent local session IDs that contain user-visible history, omitting housekeeping-only sessions. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -129,7 +129,7 @@ CompletableFuture listNonEmptySessionIds(S } /** - * GitHub task ID to look up. + * Finds the local session bound to a GitHub task ID, if any. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -140,7 +140,7 @@ public CompletableFuture findByTaskId(SessionsFindBy } /** - * UUID prefix to resolve to a unique session ID. + * Resolves a UUID prefix to a unique session ID, if exactly one session matches. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -151,7 +151,7 @@ public CompletableFuture findByPrefix(SessionsFindBy } /** - * Optional working-directory context used to score session relevance. + * Returns the most-relevant prior session for a given working-directory context. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -162,7 +162,7 @@ public CompletableFuture getLastForContext(Sess } /** - * Session ID whose event-log file path to compute. + * Computes the absolute path to a session's persisted events.jsonl file. Internal: filesystem paths are only meaningful in-process (CLI and runtime share a filesystem). Currently used by the CLI's contribution-graph feature to read historical events directly. Remote SDK consumers must not depend on this; a proper event-query API would replace it if the contribution graph ever needed to work over the wire. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -173,7 +173,7 @@ CompletableFuture getEventFilePath(SessionsGetEv } /** - * Map of sessionId -> on-disk size in bytes for each session's workspace directory. + * Returns the on-disk byte size of each session's workspace directory. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -184,7 +184,7 @@ public CompletableFuture getSizes() { } /** - * Session IDs to test for live in-use locks. + * Returns the subset of the supplied session IDs that are currently held by another running process. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -195,7 +195,7 @@ public CompletableFuture checkInUse(SessionsCheckInUse } /** - * Session ID to look up the persisted remote-steerable flag for. + * Returns a session's persisted remote-steerable flag, if any has been recorded. Internal: this is CLI-specific book-keeping used by `--continue` / `--resume` to inherit the prior session's remote-steerable preference. SDK consumers that want similar behavior should manage their own persistence around start/stop calls rather than relying on this runtime-side flag. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -206,7 +206,7 @@ CompletableFuture getPersistedRemoteS } /** - * Session ID to close. + * Closes a session: emits shutdown, flushes pending events, releases the in-use lock, and disposes the active session. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -217,7 +217,7 @@ public CompletableFuture close(SessionsCloseParams params) { } /** - * Session IDs to close, deactivate, and delete from disk. + * Closes, deactivates, and deletes a set of sessions, returning the bytes freed per session. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -228,7 +228,7 @@ public CompletableFuture bulkDelete(SessionsBulkDelete } /** - * Session ID to delete from disk. + * Deletes one local session from disk after running the same lifecycle hooks as the session manager. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -239,7 +239,7 @@ CompletableFuture delete(SessionsDeleteParams params) { } /** - * Age threshold and optional flags controlling which old sessions are pruned (or simulated when dryRun is true). + * Deletes sessions older than the given threshold, with optional dry-run and exclusion list. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -250,7 +250,7 @@ public CompletableFuture pruneOld(SessionsPruneOldParams } /** - * Session ID whose pending events should be flushed to disk. + * Flushes a session's pending events to disk. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -261,7 +261,7 @@ public CompletableFuture save(SessionsSaveParams params) { } /** - * Session ID whose in-use lock should be released. + * Releases the in-use lock held by this process for a session. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -272,7 +272,7 @@ public CompletableFuture releaseLock(SessionsReleaseLockParams params) { } /** - * Session metadata records to enrich with summary and context information. + * Backfills missing summary and context fields on the supplied session metadata records. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -283,7 +283,7 @@ public CompletableFuture enrichMetadata(SessionsEn } /** - * Active session ID and an optional flag for deferring repo-level hooks until folder trust. + * Reloads user, plugin, and (optionally) repo hooks on the active session. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -294,7 +294,7 @@ public CompletableFuture reloadPluginHooks(SessionsReloadPluginHooksParams } /** - * Active session ID whose deferred repo-level hooks should be loaded. + * Loads previously-deferred repo-level hooks on the active session, returning queued startup prompts. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -305,7 +305,7 @@ public CompletableFuture loadDeferredRepoHo } /** - * Manager-wide additional plugins to register; replaces any previously-configured set. + * Replaces the manager-wide additional plugins registered with the session manager. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -316,7 +316,7 @@ public CompletableFuture setAdditionalPlugins(SessionsSetAdditionalPlugins } /** - * Session ID whose board entry count should be returned. + * Gets the dynamic-context board entry count associated with a session, when available. Internal: this exists solely so CLI telemetry events (`rem_spawn_gate`, `rem_consolidation_complete`) can pair START / END board counts around the detached rem-agent spawn. "Dynamic context board" is a runtime-internal concept that is not part of the public SDK contract; the long-term plan is to relocate the telemetry emission into the runtime so this method can be deleted entirely. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -327,7 +327,7 @@ CompletableFuture getBoardEntryCount(SessionsG } /** - * Parameters for attaching the remote-control singleton to a session. + * Attaches the runtime-managed remote-control singleton to a session, awaiting initial setup. If remote control is already attached to a different session, the singleton is transferred (preserving the underlying Mission Control connection). Returns the final status. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -338,7 +338,7 @@ public CompletableFuture startRemoteControl(Se } /** - * Parameters for atomically rebinding the remote-control singleton. + * Atomically rebinds the remote-control singleton to a different session, preserving the underlying Mission Control connection. When `expectedFromSessionId` is provided and does not match the singleton's current `attachedSessionId`, the transfer is rejected with `transferred: false` and the current status is returned unchanged. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -349,7 +349,7 @@ public CompletableFuture transferRemoteCont } /** - * Patch for the singleton's steering state. + * Patches the steering state of the active remote-control singleton. When remote control is off, this is a no-op and the off status is returned. Today only `enabled: true` is actionable on the underlying exporter; passing `false` is reserved for future use. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -360,7 +360,7 @@ public CompletableFuture setRemoteContro } /** - * Parameters for stopping the remote-control singleton. + * Stops the remote-control singleton. When `expectedSessionId` is provided and does not match the singleton's current `attachedSessionId`, the stop is rejected with `stopped: false` and the current status is returned unchanged (unless `force` is set, in which case the singleton is unconditionally torn down). *

* Invokes the method with no params, applying the runtime defaults. * @@ -373,7 +373,7 @@ public CompletableFuture stopRemoteControl() { } /** - * Parameters for stopping the remote-control singleton. + * Stops the remote-control singleton. When `expectedSessionId` is provided and does not match the singleton's current `attachedSessionId`, the stop is rejected with `stopped: false` and the current status is returned unchanged (unless `force` is set, in which case the singleton is unconditionally torn down). * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -384,7 +384,7 @@ public CompletableFuture stopRemoteControl(Sess } /** - * Wrapper for the singleton's current status. + * Returns the current state of the remote-control singleton, including the attached session id and frontend URL when active. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -395,7 +395,7 @@ public CompletableFuture getRemoteControlS } /** - * Params to attach or detach an in-process ExtensionController delegate. + * Attaches (or detaches) an in-process ExtensionController delegate for the given session in a local host adapter. Pass `controller: undefined` to detach. Internal because the controller cannot cross the JSON-RPC boundary; the runtime manages its own session extension service. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerSkillsApi.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerSkillsApi.java index 2f77e8da88..30b35346bb 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerSkillsApi.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerSkillsApi.java @@ -34,7 +34,7 @@ public final class ServerSkillsApi { } /** - * Side-effect-free planning of one verified Agent Finder Skill candidate. + * Plans installation of a verified Agent Finder Skill candidate without writing files. The returned review is safe to present to a user and installing always leaves the Skill disabled until separately enabled. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -45,7 +45,7 @@ public CompletableFuture planInstall(SkillsPl } /** - * Applies exactly one retained verified Skill installation plan. + * Consumes one verified Skill installation plan, requests explicit human consent through installations.confirm on the original connection, then revalidates and installs the Skill disabled. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -56,7 +56,7 @@ public CompletableFuture applyInstall(SkillsApplyInstal } /** - * Read-only preparation of one owned Skill removal under fresh selected-session authority. + * Prepares a read-only removal plan for an owned verified Agent Finder Skill installation. Uninstall planning is never gated by the Skill-install feature flag. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -67,7 +67,7 @@ public CompletableFuture planUninstall(Skills } /** - * One-use application of the exact retained Skill removal plan. + * Consumes an owned Skill removal plan, requests explicit human consent through installations.confirm, refuses drift, and removes the exact owned files through quarantine. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -78,7 +78,7 @@ public CompletableFuture applyUninstall(SkillsApplyUnin } /** - * Optional project paths and additional skill directories to include in discovery. + * Discovers skills across global and project sources. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -89,7 +89,7 @@ public CompletableFuture discover(SkillsDiscoverParams par } /** - * Optional project paths to enumerate. + * Returns the canonical directories where a client may create skills that the runtime will recognize, including ones that do not exist yet. Project directories become active once created. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerSkillsConfigApi.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerSkillsConfigApi.java index c29770bf14..35cf2e2ef1 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerSkillsConfigApi.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerSkillsConfigApi.java @@ -27,7 +27,7 @@ public final class ServerSkillsConfigApi { } /** - * Skill names to mark as disabled in global configuration, replacing any previous list. + * Replaces the global list of disabled skills. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -38,7 +38,7 @@ public CompletableFuture setDisabledSkills(SkillsConfigSetDisabledSkillsPa } /** - * Adds or removes a single skill from the global disabled list, leaving every other entry untouched. + * Atomically adds or removes one skill from the disabled list. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerSkillsInstallationsApi.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerSkillsInstallationsApi.java index e0d0afe72a..51029bc363 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerSkillsInstallationsApi.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerSkillsInstallationsApi.java @@ -27,7 +27,7 @@ public final class ServerSkillsInstallationsApi { } /** - * Inventory request under an explicitly selected existing session. + * Lists owned verified Agent Finder Skill installations for the selected existing session. Listing is never gated by the Skill-install feature flag. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -38,7 +38,7 @@ public CompletableFuture list(SkillsInstallat } /** - * Inventory request under an explicitly selected existing session. + * Reconciles interrupted owned Skill installation work for the selected existing session, then inspects owned inventory. Recovery is never gated by the Skill-install feature flag. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -49,7 +49,7 @@ public CompletableFuture recover(SkillsInstal } /** - * Existing-operation control. A new session selector is deliberately not accepted. + * Inspects a known Skill installation operation on its original runtime connection. Status is never gated by the Skill-install feature flag. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -60,7 +60,7 @@ public CompletableFuture status(SkillsInstall } /** - * Existing-operation control. A new session selector is deliberately not accepted. + * Requests cancellation of a known Skill installation operation before commit. Already-started durable work requires recovery instead of silent replay. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -71,7 +71,7 @@ public CompletableFuture cancel(SkillsInstall } /** - * Persisted enablement update for one owned Skill installation. + * Atomically persists enablement for one owned Agent Finder Skill and reconciles the selected bound session. Enablement is installation-scoped by receipt identity and is never gated by the Skill-install feature flag. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerToolsApi.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerToolsApi.java index 2938010010..6f3b3b8759 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerToolsApi.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerToolsApi.java @@ -27,7 +27,7 @@ public final class ServerToolsApi { } /** - * Optional model identifier whose tool overrides should be applied to the listing. + * Lists built-in tools available for a model. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerUserSettingsApi.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerUserSettingsApi.java index 665cfb107a..d46e570346 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerUserSettingsApi.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerUserSettingsApi.java @@ -27,7 +27,7 @@ public final class ServerUserSettingsApi { } /** - * Invokes {@code user.settings.reload}. + * Drops this runtime process's in-memory user settings cache so the next settings read observes disk. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -38,7 +38,7 @@ public CompletableFuture reload() { } /** - * Per-key metadata for every known user setting (settings.json overlaid with the legacy config.json, config.json wins), including settings left at their default. Excludes repository- and enterprise-managed overrides. + * Lists every known user setting (settings.json overlaid with the legacy config.json, config.json wins), each with its effective value, its default, and whether it is at the default — so settings the user has never set still appear with their default value. Does not include repository- or enterprise-managed overrides that the runtime layers on top at session time. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -49,7 +49,7 @@ public CompletableFuture get() { } /** - * Partial user settings to write to settings.json. Each top-level key is written individually, replacing the existing value; a key whose value is null is removed. + * Writes one or more user settings to settings.json, replacing each provided top-level key. A key whose value is null is removed. Returns the keys whose new value is shadowed by a legacy config.json entry (config.json wins on read), which the runtime leaves in place — such writes do not take effect until the legacy value is removed. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionAccountsApi.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionAccountsApi.java index 76b3fdc7e0..2b4f752fdf 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionAccountsApi.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionAccountsApi.java @@ -35,7 +35,7 @@ public final class SessionAccountsApi { } /** - * Enumerate request carrying the typed collection query. + * Enumerate a typed accounts collection: the signed-in accounts, or the providers offered for interactive login. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -51,7 +51,7 @@ public CompletableFuture enumerate(SessionAccountsEnumerateP } /** - * Read request carrying the typed datum query. + * Read one typed accounts datum: the active account, a neutral status summary, or the last authentication errors. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -67,7 +67,7 @@ public CompletableFuture get(SessionAccountsGetParams params) { } /** - * Mutation request carrying the typed write command. + * Apply one non-interactive accounts mutation: switch the active account, log an account out, or set credentials from a token. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionAccountsLoginApi.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionAccountsLoginApi.java index 5537a0dad2..24e3c6a642 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionAccountsLoginApi.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionAccountsLoginApi.java @@ -31,7 +31,7 @@ public final class SessionAccountsLoginApi { } /** - * Begin an interactive login flow for a provider kind. Dispatch is kind-only. + * Begin an interactive login flow for a provider kind (dispatch is kind-only) and return its opaque flow id and first step. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -47,7 +47,7 @@ public CompletableFuture begin(SessionAccountsL } /** - * Advance an in-flight login flow, optionally fulfilling an input-required step. + * Advance an in-flight login flow, optionally fulfilling an input-required step, and return the next step. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -63,7 +63,7 @@ public CompletableFuture advance(SessionAccountsLoginAdvanceParam } /** - * Cancel an in-flight login flow. + * Cancel an in-flight login flow and release its resources. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionAgentApi.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionAgentApi.java index d2499fe3a5..0030c622d0 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionAgentApi.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionAgentApi.java @@ -31,7 +31,7 @@ public final class SessionAgentApi { } /** - * Controls whether built-in agents and authored prompt text are included. + * Lists agents available to the session. Defaults to custom agents only; pass includeBuiltInAgents to include the effective built-in agents. *

* Invokes the method with no params, applying the runtime defaults. * @@ -44,7 +44,7 @@ public CompletableFuture list() { } /** - * Controls whether built-in agents and authored prompt text are included. + * Lists agents available to the session. Defaults to custom agents only; pass includeBuiltInAgents to include the effective built-in agents. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -60,7 +60,7 @@ public CompletableFuture list(SessionAgentListParams par } /** - * An in-memory authored prompt override for an available agent. + * Sets an in-memory authored prompt override for an available agent. For built-in agents, this replaces only the static base prompt while preserving runtime-owned dynamic prompt composition and behavior. The special `general-purpose` agent is not overrideable. Overrides are not persisted; resumed and forked sessions start without them, so the host must re-apply them. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -76,7 +76,7 @@ public CompletableFuture setPrompt(SessionAgentSetPromptParams params) { } /** - * Identifies the target session. + * Gets the currently selected custom agent for the session. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -87,7 +87,7 @@ public CompletableFuture getCurrent() { } /** - * Name of the custom agent to select for subsequent turns. + * Selects a custom agent for subsequent turns in the session. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -103,7 +103,7 @@ public CompletableFuture select(SessionAgentSelectPara } /** - * Identifies the target session. + * Clears the selected custom agent and returns the session to the default agent. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -114,7 +114,7 @@ public CompletableFuture deselect() { } /** - * Identifies the target session. + * Reloads custom agent definitions and returns the refreshed list. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionAutopilotObjectiveApi.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionAutopilotObjectiveApi.java index ab4058e6b6..8edc125d7e 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionAutopilotObjectiveApi.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionAutopilotObjectiveApi.java @@ -29,7 +29,7 @@ public final class SessionAutopilotObjectiveApi { } /** - * Identifies the target session. + * Reads the current canonical autopilot objective state for this session. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionCanvasActionApi.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionCanvasActionApi.java index 8c6ce4b79e..3e41100db6 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionCanvasActionApi.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionCanvasActionApi.java @@ -31,7 +31,7 @@ public final class SessionCanvasActionApi { } /** - * Canvas action invocation parameters. + * Invokes an action on an open canvas instance. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionCanvasApi.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionCanvasApi.java index 2fce433e5c..fdcee24883 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionCanvasApi.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionCanvasApi.java @@ -38,7 +38,7 @@ public final class SessionCanvasApi { } /** - * Identifies the target session. + * Lists canvases declared for the session. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -49,7 +49,7 @@ public CompletableFuture list() { } /** - * Identifies the target session. + * Lists currently open canvas instances for the live session. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -60,7 +60,7 @@ public CompletableFuture listOpen() { } /** - * Canvas open parameters. + * Opens or focuses a canvas instance. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -76,7 +76,7 @@ public CompletableFuture open(SessionCanvasOpenParams p } /** - * Canvas close parameters. + * Closes an open canvas instance. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionCanvasProviderApi.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionCanvasProviderApi.java index c68c050f75..90a80dfce0 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionCanvasProviderApi.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionCanvasProviderApi.java @@ -31,7 +31,7 @@ final class SessionCanvasProviderApi { } /** - * Internal canvas provider registration parameters. + * Registers an internal canvas provider connection and its contributions. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -47,7 +47,7 @@ CompletableFuture register(SessionCanvasProviderRegisterParams params) { } /** - * Internal canvas provider unregistration parameters. + * Unregisters an internal canvas provider connection. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionCommandsApi.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionCommandsApi.java index 6be3682233..55cd139ae5 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionCommandsApi.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionCommandsApi.java @@ -31,7 +31,7 @@ public final class SessionCommandsApi { } /** - * Optional filters controlling which command sources to include in the listing. + * Lists slash commands available in the session. *

* Invokes the method with no params, applying the runtime defaults. * @@ -44,7 +44,7 @@ public CompletableFuture list() { } /** - * Optional filters controlling which command sources to include in the listing. + * Lists slash commands available in the session. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -60,7 +60,7 @@ public CompletableFuture list(SessionCommandsListPara } /** - * Slash command name and optional raw input string to invoke. + * Invokes a slash command in the session. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -76,7 +76,7 @@ public CompletableFuture invoke(SessionCommandsInv } /** - * The pending slash-command invocation effect to finalize, plus whether the host applied or cancelled it. + * Finalizes persistence associated with a client-applied slash-command effect. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -92,7 +92,7 @@ CompletableFuture finalizeInvocationEffe } /** - * Pending command request ID and an optional error if the client handler failed. + * Reports completion of a pending client-handled slash command. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -108,7 +108,7 @@ public CompletableFuture handlePendin } /** - * Slash command name and argument string to execute synchronously. + * Executes a slash command synchronously and returns any error. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -124,7 +124,7 @@ public CompletableFuture execute(SessionCommandsEx } /** - * Slash-prefixed command string to enqueue for FIFO processing. + * Enqueues a slash command for FIFO processing on the local session. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -140,7 +140,7 @@ public CompletableFuture enqueue(SessionCommandsEnqueuePar } /** - * Queued-command request ID and the result indicating whether the host executed it (and whether to stop processing further queued commands). + * Reports whether the host actually executed a queued command and whether to continue processing. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionCompletionsApi.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionCompletionsApi.java index 6b9d8aa252..f9fa5d1dae 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionCompletionsApi.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionCompletionsApi.java @@ -31,7 +31,7 @@ public final class SessionCompletionsApi { } /** - * Identifies the target session. + * Gets the characters that should trigger host-driven completions for the session. Empty disables host-driven completions (e.g. local sessions, or a relay host that does not advertise them). * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -42,7 +42,7 @@ public CompletableFuture getTrigge } /** - * Request host-driven completions for the current composer input. + * Requests host-driven completion items for the current composer input. Returns an empty list when the host has no items or does not support completions. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionConnectorsApi.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionConnectorsApi.java index e192274a87..e69fc59d23 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionConnectorsApi.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionConnectorsApi.java @@ -32,7 +32,7 @@ public final class SessionConnectorsApi { } /** - * Identifies the target session. + * Returns feature availability and bounded polling limits for the EXPERIMENTAL session connector API. This method never performs a Connector service request. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -43,7 +43,7 @@ public CompletableFuture getCapabilities } /** - * Identifies the target session. + * Returns the session account selection, or null. * * @return a future that completes with the {@code ConnectorSessionAccount} value, * or {@code null} when the result is absent. Callers must handle the @@ -58,7 +58,7 @@ public CompletableFuture getAccount() { } /** - * Identifies the target session. + * Returns authoritative session Connector state from current availability, pinned account selection, cached catalog, and live MCP projection without performing a Connector service request. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -69,7 +69,7 @@ public CompletableFuture getStatus() { } /** - * Pins a Connector operation to one host-owned GitHub account through its opaque selection ID. Provider tokens are never accepted. + * Returns the cached Connector catalog for the pinned opaque account selection, fetching it only when this session has no cached catalog. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -85,7 +85,7 @@ public CompletableFuture list(SessionConnectorsList } /** - * Pins a Connector operation to one host-owned GitHub account through its opaque selection ID. Provider tokens are never accepted. + * Refreshes and validates the Connector catalog for the pinned opaque account selection. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -101,7 +101,7 @@ public CompletableFuture refresh(SessionConnecto } /** - * Selects one Connector and the pinned host-owned account used for its service and MCP authorization. + * Initiates an idempotent Connector connection request without opening a browser. Returns connected when the service is immediately authoritative, consent_required with a validated URL, or pending with an opaque continuation ID. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -117,7 +117,7 @@ public CompletableFuture connect(SessionConnectorsConnec } /** - * Selects one Connector and the pinned host-owned account used for its service and MCP authorization. + * Re-initiates an idempotent Connector connection request without browser or UI effects, with the same typed outcomes as connect. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -133,7 +133,7 @@ public CompletableFuture reconnect(SessionConnectorsReco } /** - * Explicitly bounded continuation of a pending Connector connection. + * Continues a pending Connector connection with caller-supplied attempt, interval, and deadline bounds. The runtime never opens the returned consent URL. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -149,7 +149,7 @@ public CompletableFuture continueConnection(SessionConne } /** - * Selects one Connector and the pinned host-owned account used for its service and MCP authorization. + * Disconnects one Connector for the pinned opaque account selection, refreshes the authoritative catalog, and removes its session-owned MCP projection. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -165,7 +165,7 @@ public CompletableFuture disconnect(SessionCo } /** - * Requests authoritative Connector-to-MCP reconciliation for the pinned account. + * Reconciles the authoritative cached or freshly requested Connector catalog into the session Connector MCP projection and returns live status. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -181,7 +181,7 @@ public CompletableFuture reconcile(SessionConn } /** - * Requests authoritative Connector-to-MCP reconciliation for the pinned account. + * Reconciles the authoritative cached or freshly requested Connector catalog into the session Connector MCP projection and returns live status. *

* Accepts the extensible request, including inputs added after the params record. * @@ -196,7 +196,7 @@ public CompletableFuture reconcile(SessionConn } /** - * Pins a Connector operation to one host-owned GitHub account through its opaque selection ID. Provider tokens are never accepted. + * Reconciles the authoritative Connector catalog into the session MCP projection during startup with a bounded deadline and fail-closed cleanup. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -212,7 +212,7 @@ CompletableFuture reconcileForStartup(SessionConnectorsReconcil } /** - * Identifies the target session. + * Removes the runtime-owned Connector MCP projection without changing service-side connections. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionContentExclusionApi.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionContentExclusionApi.java index eb621e6b49..afddaed971 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionContentExclusionApi.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionContentExclusionApi.java @@ -31,7 +31,7 @@ public final class SessionContentExclusionApi { } /** - * Local file system absolute paths within the session working directory to check against its content-exclusion policy. + * Checks local file system absolute paths within the session working directory against its content-exclusion policy. Results preserve input order. Unsupported paths/filesystems and unavailable policy evaluation return available false, and callers must treat every requested path as excluded. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionCustomizationsApi.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionCustomizationsApi.java index ee9134f803..b0183065b0 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionCustomizationsApi.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionCustomizationsApi.java @@ -29,7 +29,7 @@ public final class SessionCustomizationsApi { } /** - * Identifies the target session. + * For local sessions, reconciles repository context and discovered instructions, plugins, skills, agents, hooks, MCP servers, and extensions after files appear or change under the working directory. Independent component failures are returned in outcomes and errors; a rejected call can have partially applied earlier steps. Remote sessions must reload on their agent host instead. The model-facing context is rebuilt on the next turn. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionDebugApi.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionDebugApi.java index b2cd030183..ed06730c49 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionDebugApi.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionDebugApi.java @@ -31,7 +31,7 @@ public final class SessionDebugApi { } /** - * Options for collecting a session debug bundle with configurable redaction. + * Collects a session debug log bundle into a local archive or staging directory. Logs are redacted by default; redaction can be configured per caller-provided diagnostic entry. The runtime includes session-owned logs by default and accepts caller-provided diagnostic entries so host applications can add their own files without changing this API shape. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionDiagnosticsApi.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionDiagnosticsApi.java index af4c57837b..d263853aec 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionDiagnosticsApi.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionDiagnosticsApi.java @@ -31,7 +31,7 @@ public final class SessionDiagnosticsApi { } /** - * Patch session diagnostic thresholds for explicitly supplied sources. + * Patches configured session diagnostic sources without restarting their producers. Setting a source level to off clears its retained diagnostics and invalidates cursors selecting that source. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -47,7 +47,7 @@ public CompletableFuture configure(SessionDia } /** - * Cursor-based request for session diagnostics. The default limit is 100 (maximum 500); the default waitMs is zero (maximum 30000). + * Reads a bounded batch of retained session diagnostics for the selected sources. Records are never consumed and each reader advances independently through its opaque cursor. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionEventLogApi.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionEventLogApi.java index 8ad9b3b1f5..87ad200719 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionEventLogApi.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionEventLogApi.java @@ -31,7 +31,7 @@ public final class SessionEventLogApi { } /** - * Cursor, batch size, and optional long-poll/filter parameters for reading session events. + * Reads a batch of session events from a cursor, optionally waiting for new events. Supports tail-first reads via `direction: backward`. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -47,7 +47,7 @@ public CompletableFuture read(SessionEventLogReadPara } /** - * Identifies the target session. + * Returns a snapshot of the current tail cursor without consuming events. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -58,7 +58,7 @@ public CompletableFuture tail() { } /** - * Event type to register consumer interest for, used by runtime gating logic. + * Registers consumer interest in an event type for runtime gating purposes. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -74,7 +74,7 @@ public CompletableFuture registerInterest } /** - * Opaque handle previously returned by `registerInterest` to release. + * Releases a consumer's previously-registered interest in an event type. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionExtensionsApi.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionExtensionsApi.java index e21c109684..cc1de448bf 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionExtensionsApi.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionExtensionsApi.java @@ -31,7 +31,7 @@ public final class SessionExtensionsApi { } /** - * Identifies the target session. + * Lists extensions discovered for the session and their current status. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -42,7 +42,7 @@ public CompletableFuture list() { } /** - * Source-qualified extension identifier to enable for the session. + * Enables an extension for the session. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -58,7 +58,7 @@ public CompletableFuture enable(SessionExtensionsEnableParams params) { } /** - * Source-qualified extension identifier to disable for the session. + * Disables an extension for the session. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -74,7 +74,7 @@ public CompletableFuture disable(SessionExtensionsDisableParams params) { } /** - * Identifies the target session. + * Reloads extension definitions and processes for the session. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -85,7 +85,7 @@ public CompletableFuture reload() { } /** - * Parameters for session.extensions.sendAttachmentsToMessage. + * Push attachments into the next user-message turn from an extension. The host should surface them as composer pills and forward them via the next session.send call. Callable only by extension-owned connections. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionFleetApi.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionFleetApi.java index ca855b70a9..1c5730cd88 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionFleetApi.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionFleetApi.java @@ -31,7 +31,7 @@ public final class SessionFleetApi { } /** - * Parameters for starting fleet orchestration: an optional user prompt combined with the fleet instructions, plus the send options forwarded to the resulting turn. + * Starts fleet mode by submitting the fleet orchestration prompt to the session. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionGitHubAuthApi.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionGitHubAuthApi.java index 94a3b8e6d0..388576154a 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionGitHubAuthApi.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionGitHubAuthApi.java @@ -32,7 +32,7 @@ public final class SessionGitHubAuthApi { } /** - * Identifies the target session. + * Gets authentication status and account metadata for the session. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -43,7 +43,7 @@ public CompletableFuture getStatus() { } /** - * New auth credentials to install on the session. Omit to leave credentials unchanged. + * Updates the session's auth credentials used for outbound model and API requests. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -59,7 +59,7 @@ public CompletableFuture setCredentials(S } /** - * Identifies the target session. + * Gets the current authentication information for internal session hosts. * * @return a future that completes with the {@code AuthIdentity} value, * or {@code null} when the result is absent. Callers must handle the @@ -74,7 +74,7 @@ CompletableFuture getCurrentAuthInfo() { } /** - * Identifies the target session. + * Gets all authentication accounts available to the internal session host. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -85,7 +85,7 @@ CompletableFuture> getAllAuthAvailable() { } /** - * Identifies the target session. + * Refreshes Copilot account metadata for the current authentication. * * @return a future that completes with the {@code AuthIdentity} value, * or {@code null} when the result is absent. Callers must handle the @@ -100,7 +100,7 @@ CompletableFuture refreshCopilotUser() { } /** - * Internal GitHub login parameters. + * Logs in a GitHub user through the internal session host. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -116,7 +116,7 @@ CompletableFuture login(SessionGitHubAuthLoginParams params) { } /** - * Parameters for switching the session's active authentication. + * Switches the session to another available authentication. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -132,7 +132,7 @@ CompletableFuture switchToAuth(SessionGitHubAuthSwitchToAuthParams params) } /** - * Identifies the target session. + * Logs out the session's current GitHub authentication. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -143,7 +143,7 @@ CompletableFuture logout() { } /** - * Parameters identifying a GitHub authentication to log out. + * Logs out a specific GitHub authentication. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -159,7 +159,7 @@ CompletableFuture logoutUser(SessionGitHubAuthLogoutUserParams params) { } /** - * Identifies the target session. + * Gets validation errors from the most recent authentication attempt. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionHistoryApi.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionHistoryApi.java index ad44d864de..ccfbfbd17f 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionHistoryApi.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionHistoryApi.java @@ -31,7 +31,7 @@ public final class SessionHistoryApi { } /** - * Optional compaction parameters. + * Compacts the session history to reduce context usage. *

* Invokes the method with no params, applying the runtime defaults. * @@ -44,7 +44,7 @@ public CompletableFuture compact() { } /** - * Optional compaction parameters. + * Compacts the session history to reduce context usage. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -60,7 +60,7 @@ public CompletableFuture compact(SessionHistoryComp } /** - * Identifier of the event to truncate to; this event and all later events are removed. + * Truncates persisted session history to a specific event. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -76,7 +76,7 @@ public CompletableFuture truncate(SessionHistoryTr } /** - * Identifies the target session. + * Lists the user turns that the session can rewind to. Never rejects for a busy session: rewind reads need the session's file-change captures to be settled, so a session that still holds active work answers with `unavailableReason: "session-busy"` and no points, which the caller can retry. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -87,7 +87,7 @@ public CompletableFuture listRewindPoints( } /** - * Event boundary to preview for conversation-and-files rewind. + * Previews the files that a conversation-and-files rewind would restore. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -103,7 +103,7 @@ public CompletableFuture previewRewind(Sessio } /** - * Boundary and mode for rewinding session history. + * Rewinds the session conversation, optionally restoring files changed by the discarded turns. Not crash-atomic: file restore and conversation truncation are separate stores, applied in that order, so a process crash between them can leave the workspace rewound while the conversation still contains the discarded turns. There is no recovery journal; re-running the same rewind is the recovery path for a crash before truncation lands, since file restore is idempotent (already-restored files are reported as skipped) and truncation is re-derived from the still-retained boundary event. After truncation lands that boundary no longer exists, so the same request is rejected; the only stage that can still be outstanding is snapshot pruning, whose failure leaves orphan snapshots the capture store tolerates. The reverse inconsistency cannot occur, because truncation is never applied before file restore succeeds. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -119,7 +119,7 @@ public CompletableFuture rewind(SessionHistoryRewind } /** - * Identifies the target session. + * Cancels any in-progress background compaction on a local session. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -130,7 +130,7 @@ public CompletableFuture cancelB } /** - * Identifies the target session. + * Aborts any in-progress manual compaction on a local session. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -141,7 +141,7 @@ public CompletableFuture abortManualC } /** - * Identifies the target session. + * Produces a markdown summary of the session's conversation context for hand-off scenarios. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -152,7 +152,7 @@ public CompletableFuture summarizeForHa } /** - * Parameters for clearing the conversation and seeding the window that replaces it. + * Clears the session's conversation history, keeping only system and developer messages, and seeds the fresh context window with a first user message. Must be called from inside a tool handler: the clear has to drop the results of the tool calls its wipe orphans, and it rejects when no tool call is in flight. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionInstructionsApi.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionInstructionsApi.java index 0a1cf9ba21..1252fa8f59 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionInstructionsApi.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionInstructionsApi.java @@ -29,7 +29,7 @@ public final class SessionInstructionsApi { } /** - * Identifies the target session. + * Gets instruction sources loaded for the session. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -40,7 +40,7 @@ public CompletableFuture getSources() { } /** - * Identifies the target session. + * For local sessions, invalidates instruction discovery and the model-facing prompt, then returns freshly discovered sources. The updated prompt takes effect on the next turn. Remote sessions must reload on their agent host instead. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionLimitPredictionApi.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionLimitPredictionApi.java index a64f82afae..b28b54bd37 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionLimitPredictionApi.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionLimitPredictionApi.java @@ -31,7 +31,7 @@ public final class SessionLimitPredictionApi { } /** - * Parameters for predicting an AI-credit session limit. Omitting `modelId` uses the session's currently selected model. + * Predicts an AI-credit session limit for the session's resolved model. Returns an unavailable result instead of falling back when the current model is unresolved auto. *

* Invokes the method with no params, applying the runtime defaults. * @@ -44,7 +44,7 @@ public CompletableFuture predict() { } /** - * Parameters for predicting an AI-credit session limit. Omitting `modelId` uses the session's currently selected model. + * Predicts an AI-credit session limit for the session's resolved model. Returns an unavailable result instead of falling back when the current model is unresolved auto. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionLspApi.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionLspApi.java index c965007d43..2b20bd44e3 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionLspApi.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionLspApi.java @@ -31,7 +31,7 @@ public final class SessionLspApi { } /** - * Parameters for (re)loading the merged LSP configuration set. + * Loads the merged LSP configuration set for the session's working directory. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionManagedSettingsApi.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionManagedSettingsApi.java index adf8568aa2..e2e4a67fe3 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionManagedSettingsApi.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionManagedSettingsApi.java @@ -29,7 +29,7 @@ public final class SessionManagedSettingsApi { } /** - * Identifies the target session. + * Waits for the live session's in-flight managed-settings application, then returns the retained effective snapshot used by runtime enforcement and by `session.managed_settings_resolved`. It does not perform another account, device, or server resolution, and rejects when resolution has not produced a snapshot. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionMcpApi.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionMcpApi.java index 4960d36f02..aca53953b4 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionMcpApi.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionMcpApi.java @@ -48,7 +48,7 @@ public final class SessionMcpApi { } /** - * Identifies the target session. + * Lists materialized MCP servers and their connection status. Cache misses may start and wait for MCP servers. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -59,7 +59,18 @@ public CompletableFuture list() { } /** - * Server name whose tool list should be returned. + * Lists effective MCP configuration without starting, restarting, authenticating, or waiting for servers. An optional live observation is from an already materialized matching server; this is not a readiness guarantee. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ + @CopilotExperimental + public CompletableFuture listConfigured() { + return caller.invoke("session.mcp.listConfigured", java.util.Map.of("sessionId", this.sessionId), SessionMcpListConfiguredResult.class); + } + + /** + * Lists the tools exposed by a connected MCP server on this session's host. This performs a live `tools/list` request. Tool UI metadata is returned independently of whether MCP Apps rendering is enabled for the session. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -75,7 +86,7 @@ public CompletableFuture listTools(SessionMcpListTool } /** - * Name of the MCP server to enable for the session. + * Enables an MCP server for the session. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -91,7 +102,7 @@ public CompletableFuture enable(SessionMcpEnableParams params) { } /** - * Name of the MCP server to enable for the session. + * Enables an MCP server for the session. *

* Accepts the extensible request, including inputs added after the params record. * @@ -106,7 +117,7 @@ public CompletableFuture enable(SessionMcpEnableRequest request) { } /** - * Name of the MCP server to disable for the session. + * Disables an MCP server for the session. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -122,7 +133,7 @@ public CompletableFuture disable(SessionMcpDisableParams params) { } /** - * Name of the MCP server to disable for the session. + * Disables an MCP server for the session. *

* Accepts the extensible request, including inputs added after the params record. * @@ -137,7 +148,7 @@ public CompletableFuture disable(SessionMcpDisableRequest request) { } /** - * Identifies the target session. + * Reloads MCP server connections for the session. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -148,7 +159,7 @@ public CompletableFuture reload() { } /** - * Identifies the target session. + * Releases any turns waiting on an in-flight MCP load without cancelling the load, letting the agent proceed while MCP servers finish connecting in the background. No-op when no MCP load is in flight or waiting turns were already released. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -159,7 +170,7 @@ public CompletableFuture moveLoadingToB } /** - * Opaque MCP reload configuration. + * Reloads MCP server connections for the session with an explicit host-provided configuration. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -175,7 +186,7 @@ CompletableFuture reloadWithConfig(SessionMcpReloadWithCo } /** - * Identifiers and raw MCP CreateMessageRequest params used to run a sampling inference. + * Runs an MCP sampling inference on behalf of an MCP server. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -191,7 +202,7 @@ public CompletableFuture executeSampling(Sessio } /** - * The requestId previously passed to executeSampling that should be cancelled. + * Cancels an in-flight MCP sampling execution by request ID. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -207,7 +218,7 @@ public CompletableFuture cancelSampling } /** - * Mode controlling how MCP server env values are resolved (`direct` or `indirect`). + * Sets how environment-variable values supplied to MCP servers are resolved (direct or indirect). *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -223,7 +234,7 @@ public CompletableFuture setEnvValueMode(Sessio } /** - * Identifies the target session. + * Removes the auto-managed `github` MCP server when present. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -234,7 +245,7 @@ public CompletableFuture removeGitHub() { } /** - * Credential-free authentication identity used to configure GitHub MCP. + * Configures the built-in GitHub MCP server for the session's current auth context. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -250,7 +261,7 @@ CompletableFuture configureGitHub(SessionMcpConfigureG } /** - * Server name and optional configuration for an individual MCP server start. Omit `config` for a config-free start-by-name of an already-configured server. + * Starts an individual MCP server on the live session. Omit `config` for a config-free start-by-name of an already-configured server (reuses the server's already-registered configuration); supply `config` to start from a caller-supplied configuration. Session-scoped and ephemeral: the server is added to this session's running set only and is reaped when the session ends. Does NOT modify persistent user configuration (`mcp.config.*`), so it does not affect future sessions. The server surfaces through `session.mcp.list` and the `session.mcp_servers_loaded` / `session.mcp_server_status_changed` events like any other server. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -266,7 +277,7 @@ public CompletableFuture startServer(SessionMcpStartServerParams params) { } /** - * Server name and optional configuration for an individual MCP server start. Omit `config` for a config-free start-by-name of an already-configured server. + * Starts an individual MCP server on the live session. Omit `config` for a config-free start-by-name of an already-configured server (reuses the server's already-registered configuration); supply `config` to start from a caller-supplied configuration. Session-scoped and ephemeral: the server is added to this session's running set only and is reaped when the session ends. Does NOT modify persistent user configuration (`mcp.config.*`), so it does not affect future sessions. The server surfaces through `session.mcp.list` and the `session.mcp_servers_loaded` / `session.mcp_server_status_changed` events like any other server. *

* Accepts the extensible request, including inputs added after the params record. * @@ -281,7 +292,7 @@ public CompletableFuture startServer(SessionMcpStartServerRequest request) } /** - * Server name and optional replacement configuration for an individual MCP server restart. Omit `config` for a config-free restart-by-name of an already-configured server. + * Restarts an individual MCP server on the live session (stops then starts). Omit `config` for a config-free restart-by-name of an already-configured server; supply `config` to restart with a replacement configuration. Session-scoped and ephemeral: does NOT modify persistent user configuration (`mcp.config.*`). *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -297,7 +308,7 @@ public CompletableFuture restartServer(SessionMcpRestartServerParams param } /** - * Server name and optional replacement configuration for an individual MCP server restart. Omit `config` for a config-free restart-by-name of an already-configured server. + * Restarts an individual MCP server on the live session (stops then starts). Omit `config` for a config-free restart-by-name of an already-configured server; supply `config` to restart with a replacement configuration. Session-scoped and ephemeral: does NOT modify persistent user configuration (`mcp.config.*`). *

* Accepts the extensible request, including inputs added after the params record. * @@ -312,7 +323,7 @@ public CompletableFuture restartServer(SessionMcpRestartServerRequest requ } /** - * Server name for an individual MCP server stop. + * Stops an individual MCP server on the session's host. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -328,7 +339,7 @@ public CompletableFuture stopServer(SessionMcpStopServerParams params) { } /** - * Server name for an individual MCP server stop. + * Stops an individual MCP server on the session's host. *

* Accepts the extensible request, including inputs added after the params record. * @@ -343,7 +354,7 @@ public CompletableFuture stopServer(SessionMcpStopServerRequest request) { } /** - * Registration parameters for an external MCP client. + * Registers a pre-connected external MCP client (e.g. IDE) on the session's host. The caller retains lifecycle ownership of the client and transport. Marked internal because the `client` and `transport` arguments are in-process MCP SDK instances that cannot be serialized across the JSON-RPC boundary; once the CLI moves on top of the SDK, external clients will be expressed as transport configs the runtime can construct itself. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -359,7 +370,7 @@ CompletableFuture registerExternalClient(SessionMcpRegisterExternalClientP } /** - * Server name identifying the external client to remove. + * Unregisters a previously registered external MCP client by server name. Marked internal as the paired companion of `registerExternalClient`: only in-process callers that registered a client this way can meaningfully unregister it. Disappears alongside `registerExternalClient`: once external clients are described to the runtime as config rather than handed in as instances, lifecycle (including deregistration) is owned entirely by the runtime. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -375,7 +386,7 @@ CompletableFuture unregisterExternalClient(SessionMcpUnregisterExternalCli } /** - * Server name to check running status for. + * Checks whether a named MCP server is currently running on the session's host. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionMcpAppsApi.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionMcpAppsApi.java index 6b932855b2..35cd4c57a3 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionMcpAppsApi.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionMcpAppsApi.java @@ -32,7 +32,7 @@ public final class SessionMcpAppsApi { } /** - * MCP server and resource URI to fetch. + * Fetch an MCP resource (typically a `ui://` MCP App bundle, per SEP-1865) from a connected server. Requires the `mcp-apps` session capability. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -48,7 +48,7 @@ public CompletableFuture readResource(SessionM } /** - * MCP server to list app-callable tools for. + * List tools that an MCP App view is allowed to call (SEP-1865 visibility filter). Returns tools whose `_meta.ui.visibility` is unset (default `["model","app"]`) or includes `"app"`. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -64,7 +64,7 @@ public CompletableFuture listTools(SessionMcpApps } /** - * MCP server, tool name, and arguments to invoke from an MCP App view. + * Call an MCP tool from an MCP App view (SEP-1865). Enforces the visibility check that prevents an app iframe from invoking model-only tools. Returns the standard MCP `CallToolResult`. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -80,7 +80,7 @@ public CompletableFuture callTool(SessionMcpAppsCallToolParams params) } /** - * Host context to advertise to MCP App guests. + * Replace the host context returned to MCP App guests on `ui/initialize`. Hosts use this to advertise theme, locale, or other metadata to the guest UI. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -96,7 +96,7 @@ public CompletableFuture setHostContext(SessionMcpAppsSetHostContextParams } /** - * Identifies the target session. + * Read the current host context advertised to MCP App guests. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -107,7 +107,7 @@ public CompletableFuture getHostContext() { } /** - * MCP server to diagnose MCP Apps wiring for. + * Diagnose MCP Apps wiring for a specific MCP server. Reports the session capability, feature-flag state, advertised extension, and how many tools have `_meta.ui` populated. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionMcpHeadersApi.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionMcpHeadersApi.java index 45679f83a4..b6c940462e 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionMcpHeadersApi.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionMcpHeadersApi.java @@ -31,7 +31,7 @@ public final class SessionMcpHeadersApi { } /** - * MCP headers refresh request id and the host response. + * Responds to a pending MCP dynamic headers refresh request. Hosts that subscribe to `mcp.headers_refresh_required` use this to provide short-lived per-server headers or to indicate that no dynamic headers are available for this refresh. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionMcpListConfiguredParams.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionMcpListConfiguredParams.java new file mode 100644 index 0000000000..d4d886b5b0 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionMcpListConfiguredParams.java @@ -0,0 +1,30 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.github.copilot.CopilotExperimental; +import javax.annotation.processing.Generated; + +/** + * Identifies the target session. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ +@CopilotExperimental +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionMcpListConfiguredParams( + /** Target session identifier */ + @JsonProperty("sessionId") String sessionId +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionMcpListConfiguredResult.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionMcpListConfiguredResult.java new file mode 100644 index 0000000000..b54b5b238c --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionMcpListConfiguredResult.java @@ -0,0 +1,31 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.github.copilot.CopilotExperimental; +import java.util.List; +import javax.annotation.processing.Generated; + +/** + * Effective MCP configuration with optional live observations from matching already materialized servers. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ +@CopilotExperimental +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionMcpListConfiguredResult( + /** Effective configured MCP servers. */ + @JsonProperty("servers") List servers +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionMcpOauthApi.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionMcpOauthApi.java index 8e90316975..3910d287eb 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionMcpOauthApi.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionMcpOauthApi.java @@ -32,7 +32,7 @@ public final class SessionMcpOauthApi { } /** - * Pending MCP OAuth request ID and host-provided token or cancellation response. + * Resolves a pending MCP OAuth request with a host-provided token or cancellation. The pending request is emitted as mcp.oauth_required with the data necessary to authorize the request. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -48,7 +48,7 @@ public CompletableFuture handlePendin } /** - * Identifies the MCP server whose persisted OAuth credentials were updated. + * Notifies the session that MCP OAuth authentication succeeded and updated credentials were persisted, so cached tool definitions can be refreshed. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -64,7 +64,7 @@ public CompletableFuture authenticationStateChanged(SessionMcpOauthAuthent } /** - * Effect-free preparation bound to the existing local session, requester and installation, with frozen options. + * Prepares an inert, expiring owned OAuth login bound to the original session requester and exact installation. Does not activate, connect, read credentials or open a browser. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -80,7 +80,7 @@ public CompletableFuture prepareLogin(Session } /** - * Remote MCP server name and optional overrides controlling reauthentication, OAuth client display name, callback handling, and static OAuth client selection. + * Starts OAuth authentication for a remote MCP server. Owned servers require the original one-use prepareLogin handle and exact installation ID; manual servers retain the existing direct login behaviour. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -96,7 +96,7 @@ public CompletableFuture login(SessionMcpOauthLoginP } /** - * Remote MCP server name and optional overrides controlling reauthentication, OAuth client display name, callback handling, and static OAuth client selection. + * Starts OAuth authentication for a remote MCP server. Owned servers require the original one-use prepareLogin handle and exact installation ID; manual servers retain the existing direct login behaviour. *

* Accepts the extensible request, including inputs added after the params record. * @@ -111,7 +111,7 @@ public CompletableFuture login(SessionMcpOauthLoginR } /** - * Host-delivered callback for a runtime-managed MCP OAuth login. + * Completes a runtime-managed MCP OAuth login after the authorization server redirects to a host-managed callback URL. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -127,7 +127,7 @@ public CompletableFuture complete(SessionMcpOauthCompleteParams params) { } /** - * Remote MCP server name for a passive OAuth status probe. + * Passively probes a configured remote MCP server to classify whether OAuth is required or a cached/override token is accepted. Does not start OAuth, emit pending OAuth requests, or mutate MCP connection state. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -143,7 +143,7 @@ public CompletableFuture probe(SessionMcpOauthProbeParams p } /** - * Remote MCP server name for a passive OAuth status probe. + * Passively probes a configured remote MCP server to classify whether OAuth is required or a cached/override token is accepted. Does not start OAuth, emit pending OAuth requests, or mutate MCP connection state. *

* Accepts the extensible request, including inputs added after the params record. * @@ -158,7 +158,7 @@ public CompletableFuture probe(SessionMcpOauthProbeRequest } /** - * Targets only the original prepared/applying owned login on this exact session requester. + * Cancels the exact owned OAuth login issued to this original session requester, without clearing shared credentials. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -174,7 +174,7 @@ public CompletableFuture cancelLogin(SessionMc } /** - * Pending MCP OAuth request id to respond to. + * Responds to a pending MCP OAuth authorization request by its request id. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionMcpPromptsApi.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionMcpPromptsApi.java index 7b47b74a21..ec60f552ca 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionMcpPromptsApi.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionMcpPromptsApi.java @@ -31,7 +31,7 @@ public final class SessionMcpPromptsApi { } /** - * MCP server whose prompts to enumerate. + * Enumerate one page of prompts a connected MCP server exposes (proxies MCP `prompts/list`). Pass `cursor` to continue from a prior result's `nextCursor`. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -47,7 +47,7 @@ public CompletableFuture list(SessionMcpPromptsList } /** - * MCP server, prompt name, and optional string-valued arguments. + * Get a prompt's messages from a connected MCP server (proxies MCP `prompts/get`). Content is preserved as opaque JSON. Does not send messages to the model, execute tools, or fetch referenced resources. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionMcpResourcesApi.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionMcpResourcesApi.java index c1a30e135d..e81f50eb80 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionMcpResourcesApi.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionMcpResourcesApi.java @@ -31,7 +31,7 @@ public final class SessionMcpResourcesApi { } /** - * MCP server and resource URI to fetch. + * Fetch an MCP resource from a connected server by URI (proxies MCP `resources/read`). *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -47,7 +47,7 @@ public CompletableFuture read(SessionMcpResources } /** - * MCP server whose resources to enumerate. + * Enumerate one page of resources a connected MCP server exposes (proxies MCP `resources/list`). Pass `cursor` to continue from a prior result's `nextCursor`. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -63,7 +63,7 @@ public CompletableFuture list(SessionMcpResources } /** - * MCP server whose resource templates to enumerate. + * Enumerate one page of resource templates a connected MCP server exposes (proxies MCP `resources/templates/list`). Pass `cursor` to continue from a prior result's `nextCursor`. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionMetadataApi.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionMetadataApi.java index 8d7f3dd753..bf97a0ec39 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionMetadataApi.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionMetadataApi.java @@ -31,7 +31,7 @@ public final class SessionMetadataApi { } /** - * Identifies the target session. + * Returns a snapshot of the session's identifying metadata, mode, agent, and remote info. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -42,7 +42,7 @@ public CompletableFuture snapshot() { } /** - * Identifies the target session. + * Returns the client-owned string metadata persisted with this local session. The metadata is not included in model context, events, telemetry, snapshots, or remote exports. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -53,7 +53,7 @@ public CompletableFuture getClientMetadata() { } /** - * Atomic patch for client-owned session metadata. Operations apply in clear, remove, then set order. The resulting bag must satisfy the ClientMetadata entry and serialized-size limits. Local storage coordinates concurrent runtime processes; custom SessionFs providers must serialize writers that access the same session from multiple processes. + * Atomically patches the client-owned string metadata persisted with this local session and returns the committed bag. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -69,7 +69,7 @@ public CompletableFuture updateClientMetadata(SessionMetadataUpd } /** - * Identifies the target session. + * Reports whether the local session is currently processing user/agent messages. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -80,7 +80,7 @@ public CompletableFuture isProcessing() { } /** - * Identifies the target session. + * Returns a snapshot of activity flags for the session. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -91,7 +91,7 @@ public CompletableFuture activity() { } /** - * Model identifier and token limits used to compute the context-info breakdown. + * Returns the token breakdown for the session's current context window for a given model. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -107,7 +107,7 @@ public CompletableFuture contextInfo(SessionMe } /** - * Identifies the target session. + * Returns the experimental per-source attribution breakdown of the session's current context window as a flat list of entries (skills, subagents, MCP servers, built-in tools, plugin rollups, system/tool-definition costs, with nesting via parentId), plus the successful compaction count. The heaviest individual messages are available separately via `metadata.getContextHeaviestMessages`. Returns null until the session has initialized its system prompt and tool metadata. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -118,7 +118,7 @@ public CompletableFuture getContextA } /** - * Parameters for the heaviest-messages query. + * Returns the largest individual messages currently in the session's context window, most-expensive first. Companion to `metadata.getContextAttribution`. Returns an empty list until the session has initialized. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -134,7 +134,7 @@ public CompletableFuture getCon } /** - * Updated working-directory/git context to record on the session. + * Records a working-directory/git context change and emits a `session.context_changed` event. For a local session, a report whose `cwd` diverges from the session's current working directory is ignored (the call still succeeds but records nothing and emits no event): a local session's working directory is authoritative and is moved via `metadata.setWorkingDirectory` (or an SDK `session.resume` that supplies a `workingDirectory`), not by this method. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -150,7 +150,7 @@ public CompletableFuture recordContextChange(SessionMetadataRecordContextC } /** - * Absolute path to set as the session's new working directory. For local sessions the path must be absolute and exist on disk: it is validated before any session state changes, and a failing validation rejects the call with nothing mutated, persisted, or emitted. Remote sessions record the path as-is. + * Updates the session's working directory. For local sessions the target is validated first (an absolute path that exists on disk) and the permission primary directory is re-based; a rejected validation fails the call before any session state changes. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -166,7 +166,7 @@ public CompletableFuture setWorkingDir } /** - * Model identifier to use when re-tokenizing the session's existing messages. + * Re-tokenizes the session's existing messages against a model and returns aggregate token totals. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionModeApi.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionModeApi.java index 0f4989a920..8c2c2d66da 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionModeApi.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionModeApi.java @@ -31,7 +31,7 @@ public final class SessionModeApi { } /** - * Identifies the target session. + * Gets the current agent interaction mode. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -42,7 +42,7 @@ public CompletableFuture get() { } /** - * Agent interaction mode to apply to the session. + * Sets the current agent interaction mode. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionModelApi.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionModelApi.java index 8d47c9c716..39c8e4df50 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionModelApi.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionModelApi.java @@ -31,7 +31,7 @@ public final class SessionModelApi { } /** - * Identifies the target session. + * Gets the session's authoritative model snapshot, including the committed Auto preference and any newer unclaimed Auto preference waiting for a future user turn. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -42,7 +42,7 @@ public CompletableFuture getCurrent() { } /** - * Target model identifier and optional reasoning effort, summary, capability overrides, and context tier. + * Switches the session to a model and optional reasoning configuration. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -58,7 +58,7 @@ public CompletableFuture switchTo(SessionModelSwitch } /** - * An Auto preference request for the session. This updates Auto configuration only; it does not change the selected model to `auto`. + * Requests an Auto preference change without changing the session's selected model. The latest unclaimed request wins; the runtime commits it only after a later prompt using the `auto` model mints a usable model and token pair. A `pending` response confirms that the request was accepted, not that it committed. Observe eventual success through `session.model_change`, failure through the ephemeral `session.auto_tier_switch_failed` event, or current unclaimed state through `session.model.getCurrent`. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -74,7 +74,7 @@ public CompletableFuture switchAutoTier(Sessio } /** - * Managed, repository, and CLI model overrides to overlay onto the session at startup. + * Resolves and applies organization-managed and repository model overlays. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -90,7 +90,7 @@ CompletableFuture applyStartupOverlay(SessionModelApplyStar } /** - * Host-supplied exact model selection IDs to allow for this running session. CAPI IDs are intersected with repository `.github/allowed_models.txt` policy; provider-qualified IDs remain exempt from repository-only policy but are restricted by this host list. Omit or pass null to clear the host restriction; an explicit empty or disjoint list is rejected. Validation and pre-selection fallback failures preserve the previous restriction. Failures after a fallback selection commits retain the new restriction and selected model; callers should inspect current session state after such an error. + * Replaces or clears the host-supplied model allowlist for a running session. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -106,7 +106,7 @@ public CompletableFuture setAllowedModels(Se } /** - * Reasoning effort level to apply to the currently selected model. + * Updates the session's reasoning effort without changing the selected model. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -122,7 +122,7 @@ public CompletableFuture setReasoningEffor } /** - * Optional listing options. + * Lists models available to this session using its own auth and integration context. Connected hosts (CLI TUI, GitHub App) should call this through the session client so remote sessions return the remote CLI's available models rather than the caller's. *

* Invokes the method with no params, applying the runtime defaults. * @@ -135,7 +135,7 @@ public CompletableFuture list() { } /** - * Optional listing options. + * Lists models available to this session using its own auth and integration context. Connected hosts (CLI TUI, GitHub App) should call this through the session client so remote sessions return the remote CLI's available models rather than the caller's. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionNameApi.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionNameApi.java index 9bfc5bea59..aeefa0df31 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionNameApi.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionNameApi.java @@ -31,7 +31,7 @@ public final class SessionNameApi { } /** - * Identifies the target session. + * Gets the session's friendly name. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -42,7 +42,7 @@ public CompletableFuture get() { } /** - * New friendly name to apply to the session. + * Sets the session's friendly name. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -58,7 +58,7 @@ public CompletableFuture set(SessionNameSetParams params) { } /** - * Auto-generated session summary to apply as the session's name when no user-set name exists. + * Persists an auto-generated session summary as the session's name when no user-set name exists. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionOptionsApi.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionOptionsApi.java index 32d4b76908..06986da2ff 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionOptionsApi.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionOptionsApi.java @@ -31,7 +31,7 @@ public final class SessionOptionsApi { } /** - * Patch of mutable session options to apply to the running session. + * Patches the genuinely-mutable subset of session options. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionPermissionsApi.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionPermissionsApi.java index aed2de53fb..eb4cc591e8 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionPermissionsApi.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionPermissionsApi.java @@ -44,7 +44,7 @@ public final class SessionPermissionsApi { } /** - * Patch of permission policy fields to apply (omit a field to leave it unchanged). + * Replaces selected permission policy fields (rules, paths, URLs, exclusions, allow-all flags) on the session. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -60,7 +60,7 @@ public CompletableFuture configure(SessionPer } /** - * Pending permission request ID and the decision to apply (approve/reject and scope). + * Provides a decision for a pending tool permission request. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -76,7 +76,7 @@ public CompletableFuture } /** - * No parameters; returns currently-pending permission requests for the session. + * Reconstructs the set of pending tool permission requests from the session's event history. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -87,7 +87,7 @@ public CompletableFuture pendingRequest } /** - * Allow-all toggle for tool permission requests, with an optional telemetry source. + * Enables or disables automatic approval of tool permission requests for the session. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -103,7 +103,7 @@ public CompletableFuture setApproveAll(Se } /** - * Permission mode to apply for the session. + * Sets the permission mode for the session. `manual` follows the normal approval flow, `assisted` attaches LLM safety recommendations, and `allow-all` automatically approves permission requests. The result returns the authoritative post-mutation mode so callers can update local state without racing the `session.permissions_changed` notification. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -119,7 +119,7 @@ public CompletableFuture setMode(SessionPermiss } /** - * No parameters. + * Returns the current permission mode for the session. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -130,7 +130,7 @@ public CompletableFuture getMode() { } /** - * Scope and add/remove instructions for modifying session- or location-scoped permission rules. + * Adds or removes session-scoped or location-scoped permission rules. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -146,7 +146,7 @@ public CompletableFuture modifyRules(Sessio } /** - * Toggles whether permission prompts should be bridged into session events for this client. + * Sets whether the client wants permission prompts bridged into session events. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -162,7 +162,7 @@ public CompletableFuture setRequired(Sessio } /** - * Clears session-scoped tool approvals and optionally clears location-scoped approvals and exact session-approved paths. + * Clears session-scoped tool approvals and, for full resets, exact session-approved paths. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -178,7 +178,7 @@ public CompletableFuture resetSes } /** - * Notification payload describing the permission prompt that the client just rendered. + * Notifies the runtime that a permission prompt UI has been shown to the user. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionPermissionsFolderTrustApi.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionPermissionsFolderTrustApi.java index 55bac08941..741e4ed737 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionPermissionsFolderTrustApi.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionPermissionsFolderTrustApi.java @@ -31,7 +31,7 @@ public final class SessionPermissionsFolderTrustApi { } /** - * Folder path to check for trust. + * Reports whether a folder is trusted according to the user's folder trust state. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -47,7 +47,7 @@ public CompletableFuture isTrusted } /** - * Folder path to add to trusted folders. + * Adds a folder to the user's trusted folders list. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionPermissionsLocationsApi.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionPermissionsLocationsApi.java index 46ce8ef4d3..37f16bf40c 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionPermissionsLocationsApi.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionPermissionsLocationsApi.java @@ -31,7 +31,7 @@ public final class SessionPermissionsLocationsApi { } /** - * Working directory to resolve into a location-permissions key. + * Resolves the permission location key and type for a working directory. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -47,7 +47,7 @@ public CompletableFuture resolve(Sessi } /** - * Working directory to load persisted location permissions for. + * Applies persisted location-scoped tool approvals and allowed directories for a working directory to this session's permission service. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -63,7 +63,7 @@ public CompletableFuture apply(SessionPe } /** - * Location-scoped tool approval to persist. + * Persists a tool approval for a permission location and applies its rules to this session's live permission service. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionPermissionsPathsApi.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionPermissionsPathsApi.java index ecf9d81b52..62ae0a193b 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionPermissionsPathsApi.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionPermissionsPathsApi.java @@ -31,7 +31,7 @@ public final class SessionPermissionsPathsApi { } /** - * No parameters; returns the session's recursive directory grants and exact session-approved paths. + * Returns the session's recursive directory grants, exact session-approved paths, and primary working directory. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -42,7 +42,7 @@ public CompletableFuture list() { } /** - * Directory path to add to the session's allowed directories. + * Adds a directory to the session's allow-list and activates conventional skill and agent definitions under it. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -58,7 +58,7 @@ public CompletableFuture add(SessionPermission } /** - * Directory path to set as the session's new primary working directory. + * Updates the session's primary working directory used by the permission policy. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -74,7 +74,7 @@ public CompletableFuture updatePrima } /** - * Path to evaluate against the session's allowed directories. + * Reports whether a path falls within any of the session's allowed directories. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -90,7 +90,7 @@ public CompletableFuture * Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionPermissionsUrlsApi.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionPermissionsUrlsApi.java index 5ca15960d2..3261b07033 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionPermissionsUrlsApi.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionPermissionsUrlsApi.java @@ -31,7 +31,7 @@ public final class SessionPermissionsUrlsApi { } /** - * Whether the URL-permission policy should run in unrestricted mode. + * Toggles the runtime's URL-permission policy between unrestricted and restricted modes. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionPlanApi.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionPlanApi.java index 7805183ddf..daf85af7db 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionPlanApi.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionPlanApi.java @@ -31,7 +31,7 @@ public final class SessionPlanApi { } /** - * Identifies the target session. + * Reads the session plan file from the workspace. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -42,7 +42,7 @@ public CompletableFuture read() { } /** - * Replacement contents to write to the session plan file. + * Writes new content to the session plan file. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -58,7 +58,7 @@ public CompletableFuture update(SessionPlanUpdateParams params) { } /** - * Identifies the target session. + * Deletes the session plan file from the workspace. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -69,7 +69,7 @@ public CompletableFuture delete() { } /** - * Identifies the target session. + * Reads todo rows from the session SQL database for plan rendering. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -80,7 +80,7 @@ public CompletableFuture readSqlTodos() { } /** - * Identifies the target session. + * Reads todo rows AND dependency edges from the session SQL database for structured progress UI. Same defensive behavior as readSqlTodos — returns empty arrays when the database, tables, or columns aren't available. Clients should call this on session start and after every `session.todos_changed` event to refresh structured-UI rendering. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionPluginsApi.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionPluginsApi.java index 6a49785b30..e041a149c1 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionPluginsApi.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionPluginsApi.java @@ -35,7 +35,7 @@ public final class SessionPluginsApi { } /** - * Identifies the target session. + * Lists globally installed, live, built-in, and enterprise-managed desired plugins using the live session's authoritative account, working directory, and retained managed policy. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -46,7 +46,7 @@ public CompletableFuture list() { } /** - * Plugin source resolved relative to the session's authoritative working directory. + * Installs a plugin using the live session's authoritative account, working directory, and retained managed policy. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -62,7 +62,7 @@ public CompletableFuture install(SessionPluginsInst } /** - * Name (or spec) of the plugin to uninstall. + * Uninstalls a plugin when permitted by the live session's retained managed policy. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -78,7 +78,7 @@ public CompletableFuture uninstall(SessionPluginsUninstallParams params) { } /** - * Name (or spec) of the plugin to update. + * Updates an installed plugin using the live session's authoritative account, working directory, and retained managed policy. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -94,7 +94,7 @@ public CompletableFuture update(SessionPluginsUpdate } /** - * Plugin names (or specs) to enable in the session's authoritative working directory. + * Enables installed plugins when permitted by the live session's retained managed policy. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -110,7 +110,7 @@ public CompletableFuture enable(SessionPluginsEnableParams params) { } /** - * Plugin names (or specs) to disable in the session's authoritative working directory. + * Disables installed plugins when permitted by the live session's retained managed policy. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -126,7 +126,7 @@ public CompletableFuture disable(SessionPluginsDisableParams params) { } /** - * Optional flags controlling which side effects the reload performs. + * Reloads the session's plugin set, refreshing MCP servers, custom agents, hooks, and skills cache so SDK-driven changes via `server.plugins.*` take effect immediately. *

* Invokes the method with no params, applying the runtime defaults. * @@ -139,7 +139,7 @@ public CompletableFuture reload() { } /** - * Optional flags controlling which side effects the reload performs. + * Reloads the session's plugin set, refreshing MCP servers, custom agents, hooks, and skills cache so SDK-driven changes via `server.plugins.*` take effect immediately. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionPluginsMarketplacesApi.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionPluginsMarketplacesApi.java index 3ec14c5cf7..c9682c21f8 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionPluginsMarketplacesApi.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionPluginsMarketplacesApi.java @@ -31,7 +31,7 @@ public final class SessionPluginsMarketplacesApi { } /** - * Identifies the target session. + * Lists registered and enterprise-managed desired marketplaces using the live session's retained policy. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -42,7 +42,7 @@ public CompletableFuture list() { } /** - * Marketplace source and optional working directory for relative-path resolution. + * Adds a marketplace when permitted by the live session's retained managed policy. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -58,7 +58,7 @@ public CompletableFuture add(SessionPlugins } /** - * Name of the marketplace to remove and an optional force flag. + * Removes a marketplace when permitted by the live session's retained managed policy. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -74,7 +74,7 @@ public CompletableFuture remove(SessionP } /** - * Name of the marketplace whose plugin catalog to fetch. + * Browses a marketplace resolved through the live session's working directory and retained managed policy. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -90,7 +90,7 @@ public CompletableFuture browse(SessionP } /** - * Optional marketplace name; omit to refresh all. + * Refreshes marketplaces resolved through the live session's working directory and retained managed policy. *

* Invokes the method with no params, applying the runtime defaults. * @@ -103,7 +103,7 @@ public CompletableFuture refresh() { } /** - * Optional marketplace name; omit to refresh all. + * Refreshes marketplaces resolved through the live session's working directory and retained managed policy. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionProviderApi.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionProviderApi.java index 26ebee8489..8d3b283405 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionProviderApi.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionProviderApi.java @@ -31,7 +31,7 @@ public final class SessionProviderApi { } /** - * Optional model identifier to scope the endpoint snapshot to. + * Returns the provider endpoint and credentials the session is currently configured to talk to, so the caller can make inference calls directly against the same backend the session uses. *

* Invokes the method with no params, applying the runtime defaults. * @@ -44,7 +44,7 @@ public CompletableFuture getEndpoint() { } /** - * Optional model identifier to scope the endpoint snapshot to. + * Returns the provider endpoint and credentials the session is currently configured to talk to, so the caller can make inference calls directly against the same backend the session uses. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -60,7 +60,7 @@ public CompletableFuture getEndpoint(SessionPr } /** - * BYOK providers and/or models to add to the session's registry at runtime. Both fields are optional; provide providers, models, or both. + * Adds BYOK providers and/or models to the session's registry at runtime, extending the additive registry built from the session's `providers`/`models` options. Both fields are optional, so a call may add providers only, models only, or both. Within a single call providers are registered before models, so a model may reference a provider added in the same call; across calls a model may reference any provider already registered (from session creation or a prior add). A model whose referenced provider is not registered by the end of the call is rejected. Newly added models become selectable via `model.list` / `model.switchTo` and are inherited by sub-agents spawned afterwards. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -76,7 +76,7 @@ public CompletableFuture add(SessionProviderAddParams } /** - * Authoritative BYOK provider and model registry snapshot to apply atomically to the session. + * Atomically updates the session's BYOK provider and model registry by applying the supplied snapshot, replacing existing entries, updating models, or removing entries absent from the snapshot. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -92,7 +92,7 @@ public CompletableFuture sync(SessionProviderSyncPara } /** - * Host-managed model selection ids to withdraw from the session's BYOK registry. + * Withdraws named host-managed models from the session's BYOK registry, leaving every other entry untouched. The scoped counterpart to `provider.sync`: a snapshot can only describe entries the caller knows about, so using it to remove one model silently withdraws rows registered by another source, such as a plugin calling `provider.add` at runtime. Naming what to remove leaves unrelated entries alone. Selection ids that are not registered are ignored, so withdrawal is idempotent. A provider is removed only when one of the withdrawn models was the last entry referencing it; a provider that simply has no models, which is the normal state while its rows are supplied by catalog discovery, is left in place. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionProvidersApi.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionProvidersApi.java index 6f6dc8b5e5..f7dde45999 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionProvidersApi.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionProvidersApi.java @@ -35,7 +35,7 @@ public final class SessionProvidersApi { } /** - * Identifies the target session. + * Returns adapter definitions and supported operations in this session's effective provider catalog, without running discovery. Does not list provider instances or select inference models. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -46,7 +46,7 @@ public CompletableFuture getCatalog() { } /** - * Provider discovery parameters. + * Discovers reachable instances using an adapter from this session's effective provider catalog and provider-specific discovery input. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -62,7 +62,7 @@ public CompletableFuture discover(SessionProvide } /** - * Provider status request parameters. + * Gets current health and version information for a discovered model-provider instance. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionProvidersModelsApi.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionProvidersModelsApi.java index b309fc7c12..3f5855d9a8 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionProvidersModelsApi.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionProvidersModelsApi.java @@ -31,7 +31,7 @@ public final class SessionProvidersModelsApi { } /** - * Provider model inventory request parameters. + * Lists models installed or otherwise available from a discovered model-provider instance. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -47,7 +47,7 @@ public CompletableFuture list(SessionProviders } /** - * A discovered instance and one of its models to translate into provider configuration. Pass back the instance and model as returned by `session.providers.discover` and `session.providers.models.list`. + * Translates a discovered model into the provider and model configuration needed to use it, and reports whether each is already registered in this session. Prepares only: it registers nothing, writes nothing, and performs no provider requests. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionQueueApi.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionQueueApi.java index 101d1726f2..e38110708f 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionQueueApi.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionQueueApi.java @@ -31,7 +31,7 @@ public final class SessionQueueApi { } /** - * Identifies the target session. + * Returns the local session's pending user-facing queued items and steering messages. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -42,7 +42,7 @@ public CompletableFuture pendingItems() { } /** - * Identifies the target session. + * Returns the internal native queue snapshot for in-process session orchestration. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -53,7 +53,7 @@ CompletableFuture snapshot() { } /** - * Parameters for moving a queued item by stable id. + * Moves an addressable queued item to a public visible position. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -69,7 +69,7 @@ public CompletableFuture moveItem(SessionQueueMoveIt } /** - * Parameters for inserting a queued message at a public visible position. + * Inserts a new queued message at a public visible position. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -85,7 +85,7 @@ public CompletableFuture insertAt(SessionQueueInsert } /** - * Parameters for removing a queued item by stable id. + * Removes an addressable queued item by its stable id. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -101,7 +101,7 @@ public CompletableFuture removeAt(SessionQueueRemove } /** - * Parameters for editing a single queued message. + * Updates the text of an addressable single-message queue item. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -117,7 +117,7 @@ public CompletableFuture updateText(SessionQueueUp } /** - * Conditional withdrawal of a single user message, from its queue or from the running turn it started. + * Atomically withdraws an unchanged user message of a local session: from the queued or steering lane while unconsumed, or from the running turn it started while the model has not answered it and nothing the user sent after it is pending. Withdrawing from the running turn interrupts that turn and removes its events from history. A client retaining the original draft may restore it only when removed is true. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -133,7 +133,7 @@ public CompletableFuture withdrawMessage(Sess } /** - * Append to one pending steering message without changing its identity or delivery position. + * Atomically appends text and attachments to an unchanged, unconsumed local steering message. Returns updated=false if delivery or withdrawal already claimed the message. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -149,7 +149,7 @@ public CompletableFuture appendSteering(Sessio } /** - * Parameters for duplicating a queued item. + * Duplicates an addressable queued item immediately after its source. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -165,7 +165,7 @@ public CompletableFuture duplicateAt(SessionQueue } /** - * Parameters for acquiring or releasing the queued-lane drain pause. Acquisition is exclusive and non-idempotent: `paused: true` against an already-paused session fails with `queue_already_paused`. The pause is never released automatically — it is not tied to the caller's lifetime, so a client that exits without sending `paused: false` leaves the lane frozen. Release is unowned: `paused: false` clears the pause for any caller, including one that never acquired it. + * Acquires or releases the queued-lane drain pause. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -181,7 +181,7 @@ public CompletableFuture setDrainPaused(SessionQueueSetDrainPausedParams p } /** - * Parameters for steering a queued message into a live turn. + * Moves an addressable queued message into the live turn's steering lane. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -197,7 +197,7 @@ public CompletableFuture sendNow(SessionQueueSendNowP } /** - * Identifies the target session. + * Reports whether the local session has native queued work pending. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -208,7 +208,7 @@ CompletableFuture hasPending() { } /** - * Inputs for starting a deferred-idle drain. + * Begins a native deferred-idle drain when background work has quiesced. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -224,7 +224,7 @@ CompletableFuture beginDeferredIdleDrain(Sess } /** - * Inputs for completing a deferred-idle drain. + * Finishes a native deferred-idle drain and reports whether to drain queue work or emit idle. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -240,7 +240,7 @@ CompletableFuture finishDeferredIdleDrain(Se } /** - * Inputs for marking session.idle deferred in native state. + * Marks session.idle as deferred by native background work state. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -256,7 +256,7 @@ CompletableFuture deferSessionIdle(SessionQueueDeferSessionIdleParams para } /** - * Identifies the target session. + * Removes the most recently queued user-facing item (LIFO). * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -267,7 +267,7 @@ public CompletableFuture removeMostRecent() } /** - * Identifies the target session. + * Clears all pending queued items on the local session. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -278,7 +278,7 @@ public CompletableFuture clear() { } /** - * Internal filter for consuming queued system notifications. + * Consumes queued native system notifications matching an internal filter. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -294,7 +294,7 @@ CompletableFuture consumeSystemNotifications(Sessio } /** - * Identifies the target session. + * Enqueues the internal resume-pending wake item when orphan handling needs a follow-up turn. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -305,7 +305,7 @@ CompletableFuture enqueueResumePending() { } /** - * Identifies the target session. + * Drains the native local-session work queue for in-process session orchestration. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionRemoteApi.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionRemoteApi.java index bb1cfa0e25..015c69f830 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionRemoteApi.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionRemoteApi.java @@ -31,7 +31,7 @@ public final class SessionRemoteApi { } /** - * Optional remote session mode ("off", "export", or "on"); defaults to enabling both export and remote steering. + * Enables remote session export or steering. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -47,7 +47,7 @@ public CompletableFuture enable(SessionRemoteEnablePa } /** - * Identifies the target session. + * Disables remote session export and steering. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -58,7 +58,7 @@ public CompletableFuture disable() { } /** - * New remote-steerability state to persist as a `session.remote_steerable_changed` event. + * Persists a remote-steerability change emitted by the host as a session event. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionRpc.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionRpc.java index 9c235ef2e3..5ff78a1dc3 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionRpc.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionRpc.java @@ -180,7 +180,7 @@ public SessionRpc(RpcCaller caller, String sessionId) { } /** - * Identifies the target session. + * Suspends the session while preserving persisted state for later resume. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -191,7 +191,7 @@ public CompletableFuture suspend() { } /** - * Parameters for sending a user message to the session + * Sends a user message to the session and returns its message ID. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -207,7 +207,7 @@ public CompletableFuture send(SessionSendParams params) { } /** - * Parameters for sending zero or more user messages to the session in a single turn. Remote-backed (Mission Control) sessions do not support this method and will return an error. + * Sends zero or more user messages to the session in a single turn and returns their message IDs. All provided messages are appended to the conversation in order, then exactly one agent turn runs over the resulting history. When the list is empty, one turn runs over the existing history with no new user message. Remote-backed (Mission Control) sessions do not support this method and will return an error. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -223,7 +223,7 @@ public CompletableFuture sendMessages(SessionSendMess } /** - * Internal request for sending a system notification. + * Queues or sends an internal system notification to the session according to its passive policy. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -239,7 +239,7 @@ CompletableFuture sendSystemNotification(SessionSendSystemNotificationPara } /** - * Parameters for aborting the current turn + * Aborts the current agent turn. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -255,7 +255,7 @@ public CompletableFuture abort(SessionAbortParams params) { } /** - * Parameters for interrupting the main agent turn. + * Interrupts the current main agent turn while leaving running background work (subagents, sidekicks, and promoted attached shells) alive. No-op when the main loop is not processing. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -271,7 +271,7 @@ public CompletableFuture interruptMainTurn(Sessi } /** - * Identifies the target session. + * Cancels every running background agent (task-registry subagents plus sidekick agents) without interrupting the main agent loop. Promoted attached shells are left running. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -282,7 +282,7 @@ public CompletableFuture cancelAllBackgroundAgents() { } /** - * Parameters for shutting down the session + * Shuts down the session and persists its final state. Awaits any deferred sessionEnd hooks before resolving so user-supplied hook scripts complete before the runtime tears down. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -298,7 +298,7 @@ public CompletableFuture shutdown(SessionShutdownParams params) { } /** - * Message text, optional severity level, persistence flag, optional follow-up URL, and optional tip. + * Emits a user-visible session log event. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionSandboxApi.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionSandboxApi.java index a13ef559ef..2ace81bb8e 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionSandboxApi.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionSandboxApi.java @@ -31,7 +31,7 @@ public final class SessionSandboxApi { } /** - * Identifies the target session. + * Returns whether managed policy requires sandbox enforcement and whether an enforcement failure has permanently blocked the session. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -42,7 +42,7 @@ public CompletableFuture getEnforcemen } /** - * Request to disable sandboxing for the current session while resolving an active sandbox-bypass permission prompt. + * Disables sandboxing for the remainder of the current session and approves the referenced pending sandbox-bypass permission request. The request is rejected unless the exact request is still pending and the effective sandbox policy permits bypass. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -58,7 +58,7 @@ public CompletableFuture disableForSessio } /** - * Request to accept the sandbox path grant offered on an active sandbox escalation permission prompt. + * Adds the path offered by a pending sandbox escalation permission request's sandboxPathGrant to the session's sandbox policy and approves the request, so the blocked operation re-runs inside the sandbox rather than outside it. The request is rejected unless the exact request is still pending, carries a sandboxPathGrant, and the grant still takes effect under the current managed policy. Does not persist the path; hosts that store sandbox settings save it themselves. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionScheduleApi.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionScheduleApi.java index a6e8ad88d7..0b6a90aca9 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionScheduleApi.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionScheduleApi.java @@ -31,7 +31,7 @@ public final class SessionScheduleApi { } /** - * Identifies the target session. + * Lists the session's currently active scheduled prompts. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -42,7 +42,7 @@ public CompletableFuture list() { } /** - * Identifies the target session. + * Hydrates the native schedule registry from persisted session events. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -53,7 +53,7 @@ CompletableFuture hydrate() { } /** - * Identifies the target session. + * Reports whether the session has an active self-paced scheduled prompt. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -64,7 +64,7 @@ CompletableFuture hasSelfPaced() { } /** - * Register a relative-interval scheduled prompt. + * Registers a relative-interval scheduled prompt. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -80,7 +80,7 @@ CompletableFuture add(SessionScheduleAddParams params) { } /** - * Register a cron scheduled prompt. + * Registers a recurring cron scheduled prompt. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -96,7 +96,7 @@ CompletableFuture addCron(SessionScheduleAddCronParams params } /** - * Register an absolute-time scheduled prompt. + * Registers an absolute-time scheduled prompt. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -112,7 +112,7 @@ CompletableFuture addAt(SessionScheduleAddAtParams params) { } /** - * Register a self-paced scheduled prompt. + * Registers a self-paced scheduled prompt. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -128,7 +128,7 @@ CompletableFuture addSelfPaced(SessionScheduleAddSelfPacedPar } /** - * Re-arm a self-paced scheduled prompt. + * Re-arms an active self-paced scheduled prompt. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -144,7 +144,7 @@ CompletableFuture rearmSelfPaced(SessionScheduleRearmSelfPace } /** - * Identifier of the scheduled prompt to remove. + * Removes a scheduled prompt by id. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionSettingsApi.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionSettingsApi.java index 213b7de0a4..f07d028f93 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionSettingsApi.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionSettingsApi.java @@ -31,7 +31,7 @@ final class SessionSettingsApi { } /** - * Identifies the target session. + * Returns a redacted snapshot of session runtime settings, with secrets and raw feature flags excluded. Internal: the runtime settings shape is a runtime-internal surface and is deliberately kept out of the public SDK, because consumers should not depend on the runtime's internal settings layout. It remains callable in-process and is expected to be reworked as the runtime internals are consolidated. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -42,7 +42,7 @@ CompletableFuture snapshot() { } /** - * Named Rust-owned settings predicate to evaluate for this session. + * Evaluates a named Rust-owned settings predicate without exposing raw feature flags. Internal: the raw feature-flag names and composition are runtime-internal, so this predicate-evaluation helper is kept out of the public SDK surface and is callable in-process only. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionShellApi.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionShellApi.java index ebfd1a9c7d..6d3187ef75 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionShellApi.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionShellApi.java @@ -31,7 +31,7 @@ public final class SessionShellApi { } /** - * Shell command to run, with optional working directory and timeout in milliseconds. Spawn failures return an RPC error. + * Starts a shell command, returning an RPC error if it cannot be spawned. The command runs as the leader of its own process group (POSIX) or in a dedicated job object (Windows), so a forced termination — via "shell.kill", the request timeout, or session disposal — signals that whole group/job rather than only the direct child. Two gaps are worth planning for: a command that exits on its own does not trigger that teardown, and on POSIX a descendant that moves itself into a new session or process group (for example via "setsid") leaves the signalled group, so either can leave a background process running. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -47,7 +47,7 @@ public CompletableFuture exec(SessionShellExecParams par } /** - * Identifier of a process previously returned by "shell.exec" and the signal to send. + * Sends a signal to a shell process previously started via "shell.exec". The signal targets the command's whole process group (POSIX) or job object (Windows), so descendants still in that group are signalled too, not just the direct child. On POSIX a descendant that moved itself into a new session or process group (for example via "setsid") is no longer in the signalled group and survives. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -63,7 +63,7 @@ public CompletableFuture kill(SessionShellKillParams par } /** - * User-requested shell command and cancellation handle. + * Executes a user-requested shell command through the session runtime. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -79,7 +79,7 @@ public CompletableFuture executeUserRequ } /** - * User-requested shell execution cancellation handle. + * Cancels a user-requested shell command by request ID. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionSkillsApi.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionSkillsApi.java index f6cceb98b9..e21c294b31 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionSkillsApi.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionSkillsApi.java @@ -31,7 +31,7 @@ public final class SessionSkillsApi { } /** - * Identifies the target session. + * Lists skills available to the session. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -42,7 +42,7 @@ public CompletableFuture list() { } /** - * Identifies the target session. + * Returns the skills that have been invoked during this session. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -53,7 +53,7 @@ public CompletableFuture getInvoked() { } /** - * Name of the skill to enable for the session. + * Enables a skill for the session. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -69,7 +69,7 @@ public CompletableFuture enable(SessionSkillsEnableParams params) { } /** - * Name of the skill to disable for the session. + * Disables a skill for the session. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -85,7 +85,7 @@ public CompletableFuture disable(SessionSkillsDisableParams params) { } /** - * Identifies the target session. + * Reloads skill definitions for the session. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -96,7 +96,7 @@ public CompletableFuture reload() { } /** - * Identifies the target session. + * Ensures the session's skill definitions have been loaded from disk. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionTasksApi.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionTasksApi.java index c6ca1335de..9e78691586 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionTasksApi.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionTasksApi.java @@ -31,7 +31,7 @@ public final class SessionTasksApi { } /** - * Agent type, prompt, name, and optional description and model override for the new task. + * Starts a background agent task in the session. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -47,7 +47,7 @@ public CompletableFuture startAgent(SessionTasksSt } /** - * Identifies the target session. + * Lists background tasks tracked by the session. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -58,7 +58,7 @@ public CompletableFuture list() { } /** - * Registers or reclaims a client-owned task. + * Registers a client-owned task, or reclaims an orphaned task belonging to the same extension principal. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -74,7 +74,7 @@ public CompletableFuture register(SessionTasksRegist } /** - * Updates a client-owned task. + * Publishes generic progress or a terminal outcome for a client-owned task. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -90,7 +90,7 @@ public CompletableFuture update(SessionTasksUpdatePara } /** - * Identifies the target session. + * Refreshes metadata for any detached background shells the runtime knows about. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -101,7 +101,7 @@ public CompletableFuture refresh() { } /** - * Identifies the target session. + * Waits for all in-flight background tasks and any follow-up turns to settle. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -112,7 +112,7 @@ public CompletableFuture waitForPending() { } /** - * Identifier of the background task to fetch progress for. + * Returns progress information for a background task by ID. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -128,7 +128,7 @@ public CompletableFuture getProgress(SessionTasks } /** - * Identifies the target session. + * Returns the first sync-waiting task that can currently be promoted to background mode. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -139,7 +139,7 @@ public CompletableFuture getCurrentPromo } /** - * Identifier of the task to promote to background mode. + * Promotes an eligible synchronously-waited task so it continues running in the background. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -155,7 +155,7 @@ public CompletableFuture promoteToBackgro } /** - * Identifies the target session. + * Atomically promotes the first promotable sync-waiting task to background mode and returns it. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -166,7 +166,7 @@ public CompletableFuture promoteCu } /** - * Identifier of the background task to cancel. + * Cancels a background task. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -182,7 +182,7 @@ public CompletableFuture cancel(SessionTasksCancelPara } /** - * Identifier of the completed or cancelled task to remove from tracking. + * Removes a completed or cancelled background task from tracking. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -198,7 +198,7 @@ public CompletableFuture remove(SessionTasksRemovePara } /** - * Identifier of the target agent task, message content, and optional sender agent ID. + * Sends a message to a background agent task. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionTelemetryApi.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionTelemetryApi.java index 42589661ef..d74f8366da 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionTelemetryApi.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionTelemetryApi.java @@ -31,7 +31,7 @@ public final class SessionTelemetryApi { } /** - * Identifies the target session. + * Gets the telemetry engagement ID currently associated with the session, when available. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -42,7 +42,7 @@ public CompletableFuture getEngagementId( } /** - * Feature override key/value pairs to attach to subsequent telemetry events from this session. + * Sets feature override key/value pairs to attach to subsequent telemetry events for the session. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionToolsApi.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionToolsApi.java index 62593a3588..f8b4e32601 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionToolsApi.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionToolsApi.java @@ -31,7 +31,7 @@ public final class SessionToolsApi { } /** - * A tool name and arguments to execute through the session's native invocation pipeline. + * Executes one tool from the session's currently offered tool set through the native invocation pipeline. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -47,7 +47,7 @@ public CompletableFuture execute(SessionToolsExecuteParams params) { } /** - * Options controlling how Rust-owned built-in tool descriptors are materialized. + * Returns the Rust-owned built-in tool descriptors used to construct the session's offered tool set. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -63,7 +63,7 @@ public CompletableFuture getBuiltinDesc } /** - * Task-completion tool arguments and final result used to build a label-safe session event payload. + * Projects a completed task_complete tool call into its label-safe session event payload. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -79,7 +79,7 @@ public CompletableFuture taskCompleteEv } /** - * Pending external tool call request ID, with the tool result or an error describing why it failed. + * Provides the result for a pending external tool call. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -95,7 +95,7 @@ public CompletableFuture handlePendingT } /** - * Identifies the target session. + * Resolves, builds, and validates the runtime tool list for the session. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -106,7 +106,7 @@ public CompletableFuture initializeAndValidate() { } /** - * Identifies the target session. + * Returns lightweight metadata for the session's currently initialized tools. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -117,7 +117,7 @@ public CompletableFuture getCurrentMetadat } /** - * Complete externally implemented tool list for the calling connection. An empty list removes every tool previously supplied by that connection. + * Atomically replaces the complete externally implemented tool list supplied by the calling connection. Built-in, MCP/plugin, extension-discovered, subagent, and tools supplied by other connections remain unchanged. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -133,7 +133,7 @@ public CompletableFuture set(SessionToolsSetParams params) { } /** - * Subagent settings to apply to the current session + * Sets the current session's live subagent settings override, which takes precedence over persisted user settings until cleared. Persisted user settings remain the source of truth for future sessions. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionUiApi.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionUiApi.java index 7feccbd56a..5187b51774 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionUiApi.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionUiApi.java @@ -31,7 +31,7 @@ public final class SessionUiApi { } /** - * Transient question to answer without adding it to conversation history. + * Runs a transient no-tools model query against the current conversation context. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -47,7 +47,7 @@ public CompletableFuture ephemeralQuery(SessionUi } /** - * Prompt message and JSON schema describing the form fields to elicit from the user. + * Requests structured input from a UI-capable client. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -63,7 +63,7 @@ public CompletableFuture elicitation(SessionUiElicit } /** - * Pending elicitation request ID and the user's response (accept/decline/cancel + form values). + * Provides the user response for a pending elicitation request. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -79,7 +79,7 @@ public CompletableFuture handlePendingE } /** - * Pending elicitation request ID and the user's response (accept/decline/cancel + form values). + * Resolves a pending elicitation request after direct interaction in the trusted in-process client. Only an accepted response to the built-in ask_user tool can become trusted human evidence. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -95,7 +95,7 @@ CompletableFuture handleHumanAskUser(SessionUiHandleHumanAs } /** - * Request ID of a pending `user_input.requested` event and the user's response. + * Resolves a pending `user_input.requested` event with the user's response. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -111,7 +111,7 @@ public CompletableFuture handlePendingUse } /** - * Request ID of a pending `user_input.requested` event and the user's response. + * Resolves a pending `user_input.requested` event after direct interaction in the trusted in-process client. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -127,7 +127,7 @@ CompletableFuture handleHumanUserInput(SessionUiHandleHum } /** - * Request ID of a pending `sampling.requested` event and an optional sampling result payload (omit to reject). + * Resolves a pending `sampling.requested` event with a sampling result, or rejects it. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -143,7 +143,7 @@ public CompletableFuture handlePendingSamp } /** - * Request ID of a pending `auto_mode_switch.requested` event and the user's response. + * Resolves a pending `auto_mode_switch.requested` event with the user's accept/decline decision. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -159,7 +159,7 @@ public CompletableFuture handlePendi } /** - * Request ID of a pending `session_limits_exhausted.requested` event and the user's selected limit action. + * Resolves a pending `session_limits_exhausted.requested` event with the user's selected limit action. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -175,7 +175,7 @@ public CompletableFuture han } /** - * Request ID of a pending `exit_plan_mode.requested` event and the user's response. + * Resolves a pending `exit_plan_mode.requested` event with the user's response. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -191,7 +191,7 @@ public CompletableFuture handlePending } /** - * Request ID of a pending `exit_plan_mode.requested` event and the user's response. + * Resolves a pending `exit_plan_mode.requested` event after direct interaction in the trusted in-process client. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -207,7 +207,7 @@ CompletableFuture handleHumanExitPlanMode(SessionUiHandle } /** - * Identifies the target session. + * Registers an in-process handler for auto-mode-switch requests so the server bridge skips dispatch. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -218,7 +218,7 @@ public CompletableFuture reg } /** - * Opaque handle previously returned by `registerDirectAutoModeSwitchHandler` to release. + * Unregisters a previously-registered in-process auto-mode-switch handler by its opaque handle. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionUsageApi.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionUsageApi.java index 16ded9d6b6..159fc0993c 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionUsageApi.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionUsageApi.java @@ -29,7 +29,7 @@ public final class SessionUsageApi { } /** - * Identifies the target session. + * Gets accumulated usage metrics for the session. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionVisibilityApi.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionVisibilityApi.java index 54f38c2614..b942828e10 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionVisibilityApi.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionVisibilityApi.java @@ -31,7 +31,7 @@ public final class SessionVisibilityApi { } /** - * Identifies the target session. + * Returns the session's current Mission Control sharing status and shareable GitHub URL. Reflects whether the synced session is visible to repository readers ("repo") or restricted to its creator and collaborators ("unshared"). * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -42,7 +42,7 @@ public CompletableFuture get() { } /** - * Desired sharing status for the session. + * Sets the session's Mission Control sharing status, controlling whether the synced session is visible to repository readers. Returns the effective status and shareable GitHub URL after the change. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionWorkflowApi.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionWorkflowApi.java index 2b9e81e42f..e746033b39 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionWorkflowApi.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionWorkflowApi.java @@ -35,7 +35,7 @@ public final class SessionWorkflowApi { } /** - * Parameters for invoking a registered workflow. + * Runs a registered dynamic workflow by name at the top level. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -51,7 +51,7 @@ public CompletableFuture run(SessionWorkflowRunParams } /** - * Parameters for resuming a workflow run from its persisted identity. + * Resumes a dynamic workflow run using its persisted name, arguments, journal, and accounting. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -67,7 +67,7 @@ public CompletableFuture resume(SessionWorkflowResu } /** - * Internal parameters for invoking a registered workflow from a tool. + * Internal tool-originated dynamic workflow invocation. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -83,7 +83,7 @@ CompletableFuture runFromTool(SessionWorkflowRunFromToolParam } /** - * Internal parameters for resuming a workflow run from a tool. + * Internal tool-originated dynamic workflow resume. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -99,7 +99,7 @@ CompletableFuture resumeFromTool(SessionWorkflowResumeFrom } /** - * Parameters for retrieving a workflow run. + * Gets the current or settled envelope for a dynamic workflow run. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -115,7 +115,7 @@ public CompletableFuture getRun(SessionWorkflowGetR } /** - * Parameters for paging workflow runs. + * Lists durable dynamic workflow runs for this session in creation order. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -131,7 +131,7 @@ public CompletableFuture listRuns(SessionWorkflow } /** - * Parameters for retrieving a workflow run. + * Gets durable and live observability detail for one dynamic workflow run. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -147,7 +147,7 @@ public CompletableFuture getRunDetail(Session } /** - * Parameters for paging workflow progress. + * Pages durable progress for one dynamic workflow run. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -163,7 +163,7 @@ public CompletableFuture getRunProgress(Ses } /** - * Parameters for cancelling a workflow run. + * Requests cancellation of a dynamic workflow run and returns its run envelope. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -179,7 +179,7 @@ public CompletableFuture cancel(SessionWorkflowCanc } /** - * Parameters for pausing a running workflow. + * Pauses a running dynamic workflow and returns its settled run envelope. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -195,7 +195,7 @@ public CompletableFuture pause(SessionWorkflowPauseP } /** - * Parameters for an owned durable pause checkpoint. + * Atomically pauses an owned dynamic workflow attempt at a durable checkpoint. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -211,7 +211,7 @@ CompletableFuture pauseAtCheckpoint(SessionWorkfl } /** - * Parameters for recording workflow progress. + * Records a batch of ordered dynamic workflow progress lines. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -227,7 +227,7 @@ public CompletableFuture log(SessionWorkflowLogParams params) { } /** - * Parameters for one workflow-scoped subagent call. + * Runs one dynamic-workflow-scoped subagent and returns its result. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionWorkflowJournalApi.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionWorkflowJournalApi.java index 0909931439..70bc1b47e2 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionWorkflowJournalApi.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionWorkflowJournalApi.java @@ -31,7 +31,7 @@ public final class SessionWorkflowJournalApi { } /** - * Parameters for reading a workflow journal entry. + * Reads a memoized dynamic workflow journal entry. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -47,7 +47,7 @@ public CompletableFuture get(SessionWorkflowJou } /** - * Parameters for storing a workflow journal entry. + * Stores a memoized dynamic workflow journal entry. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionWorkspacesApi.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionWorkspacesApi.java index 71a3d1ea1b..889f4a0de2 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionWorkspacesApi.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionWorkspacesApi.java @@ -31,7 +31,7 @@ public final class SessionWorkspacesApi { } /** - * Identifies the target session. + * Gets current workspace metadata for the session. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -42,7 +42,7 @@ public CompletableFuture getWorkspace() { } /** - * Workspace metadata fields to update. + * Updates workspace metadata for a local session and returns the refreshed workspace. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -58,7 +58,7 @@ public CompletableFuture updateMetadata(S } /** - * Optional session context used when creating a local workspace. + * Ensures a local session workspace exists and returns it. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -74,7 +74,7 @@ public CompletableFuture ensure(SessionWorkspaces } /** - * Identifies the target session. + * Lists files stored in the session workspace files directory. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -85,7 +85,7 @@ public CompletableFuture listFiles() { } /** - * Relative path of the workspace file to read. + * Reads a file from the session workspace files directory. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -101,7 +101,7 @@ public CompletableFuture readFile(SessionWorksp } /** - * Relative path and UTF-8 content for the workspace file to create or overwrite. + * Creates or overwrites a file in the session workspace files directory. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -117,7 +117,7 @@ public CompletableFuture createFile(SessionWorkspacesCreateFileParams para } /** - * Relative path of the workspace file or directory to inspect. + * Returns metadata for a file or directory in the session workspace files directory. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -133,7 +133,7 @@ public CompletableFuture statFile(SessionWorksp } /** - * Directory to create within the session workspace files directory. + * Creates a directory in the session workspace files directory. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -149,7 +149,7 @@ public CompletableFuture createDirectory(SessionWorkspacesCreateDirectoryP } /** - * File or directory to remove from the session workspace files directory. + * Removes a file or directory from the session workspace files directory. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -165,7 +165,7 @@ public CompletableFuture removePath(SessionWorkspacesRemovePathParams para } /** - * Source and destination paths for a rename within the session workspace files directory. + * Renames a file or directory within the session workspace files directory. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -181,7 +181,7 @@ public CompletableFuture renamePath(SessionWorkspacesRenamePathParams para } /** - * Identifies the target session. + * Lists workspace checkpoints in chronological order. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -192,7 +192,7 @@ public CompletableFuture listCheckpoints } /** - * Checkpoint number to read. + * Reads the content of a workspace checkpoint by number. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -208,7 +208,7 @@ public CompletableFuture readCheckpoint(S } /** - * Compaction summary checkpoint to persist. + * Adds a compaction summary checkpoint to the local session workspace. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -224,7 +224,7 @@ public CompletableFuture addSummary(SessionWo } /** - * Rollback point for local workspace summaries. + * Truncates local workspace compaction summaries after a rollback. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -240,7 +240,7 @@ public CompletableFuture truncateSumma } /** - * Identifies the target session. + * Reads the autopilot objective state file from the local session workspace. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -251,7 +251,7 @@ public CompletableFuture readAuto } /** - * Autopilot objective file content to persist. + * Writes the autopilot objective state file in the local session workspace. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -267,7 +267,7 @@ public CompletableFuture writeAu } /** - * Identifies the target session. + * Deletes the autopilot objective state file from the local session workspace. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -278,7 +278,7 @@ public CompletableFuture delete } /** - * Identifies the target session. + * Checks whether the local session workspace has an autopilot objective state file. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -289,7 +289,7 @@ public CompletableFuture autopi } /** - * Pasted content to save as a UTF-8 file in the session workspace. + * Saves pasted content as a UTF-8 file in the session workspace. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -305,7 +305,7 @@ public CompletableFuture saveLargePaste(S } /** - * Parameters for computing a workspace diff. + * Computes a diff for the session workspace. Never rejects for a busy session: a `session`-mode diff that cannot read the session's file-change captures falls back to an unstaged git diff with `isFallback: true` and reports why in `unavailableReason`. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. diff --git a/java/sdk/src/test/java/com/github/copilot/generated/rpc/GeneratedRpcApiCoverageTest.java b/java/sdk/src/test/java/com/github/copilot/generated/rpc/GeneratedRpcApiCoverageTest.java index 3eb27a9178..c925bb80b0 100644 --- a/java/sdk/src/test/java/com/github/copilot/generated/rpc/GeneratedRpcApiCoverageTest.java +++ b/java/sdk/src/test/java/com/github/copilot/generated/rpc/GeneratedRpcApiCoverageTest.java @@ -63,164 +63,184 @@ void newRpcEndpoints_dispatch_expected_method_params_and_result_type(String expe } static Stream newRpcDispatches() { - return Stream.of( - dispatch("catalog.select", CatalogSelectionResult.class, """ - {"contract":{"protocolVersion":3,"requiredCapabilities":["catalog-selection"]}, - "sessionId":"catalog-session","selectionRef":"selection-1","outcome":"selected"} - """, - (server, session) -> server.catalog.select( - new CatalogSelectParams(new CatalogClientContract(3L, List.of("catalog-selection")), - "catalog-session", "selection-1", CatalogSelectionDecision.SELECTED))), - dispatch("session.managedSettings.get", SessionManagedSettingsGetResult.class, """ + return Stream + .of(dispatch("session.mcp.list", SessionMcpListResult.class, """ {"sessionId":"bound-session"} - """, (server, session) -> session.managedSettings.get()), - dispatch("session.instructions.reload", SessionInstructionsGetSourcesResult.class, """ - {"sessionId":"bound-session"} - """, (server, session) -> session.instructions.reload()), - dispatch("session.customizations.reload", CustomizationsReloadResult.class, """ - {"sessionId":"bound-session"} - """, (server, session) -> session.customizations.reload()), - dispatch("session.plugins.install", SessionPluginsInstallResult.class, """ - {"sessionId":"bound-session","source":"plugin@market"} - """, - (server, session) -> session.plugins - .install(new SessionPluginsInstallParams("foreign-session", "plugin@market"))), - dispatch("session.plugins.uninstall", Void.class, """ - {"sessionId":"bound-session","name":"plugin","directSourceId":"source-1"} - """, - (server, session) -> session.plugins - .uninstall(new SessionPluginsUninstallParams("foreign-session", "plugin", "source-1"))), - dispatch("session.plugins.update", SessionPluginsUpdateResult.class, """ - {"sessionId":"bound-session","name":"plugin@market"} - """, - (server, session) -> session.plugins - .update(new SessionPluginsUpdateParams("foreign-session", "plugin@market"))), - dispatch("session.plugins.enable", Void.class, """ - {"sessionId":"bound-session","names":["first@market","second@market"]} - """, - (server, session) -> session.plugins.enable(new SessionPluginsEnableParams("foreign-session", - List.of("first@market", "second@market")))), - dispatch("session.plugins.disable", Void.class, """ - {"sessionId":"bound-session","names":["plugin@market"]} - """, - (server, session) -> session.plugins - .disable(new SessionPluginsDisableParams("foreign-session", List.of("plugin@market")))), - dispatch("session.plugins.marketplaces.list", SessionPluginsMarketplacesListResult.class, """ - {"sessionId":"bound-session"} - """, (server, session) -> session.plugins.marketplaces.list()), - dispatch("session.plugins.marketplaces.add", SessionPluginsMarketplacesAddResult.class, """ - {"sessionId":"bound-session","source":"./market","workingDirectory":"/workspace"} - """, - (server, session) -> session.plugins.marketplaces.add( + """, (server, session) -> session.mcp.list()), + dispatch("session.mcp.listConfigured", SessionMcpListConfiguredResult.class, """ + {"sessionId":"bound-session"} + """, (server, session) -> session.mcp.listConfigured()), + dispatch("catalog.select", CatalogSelectionResult.class, """ + {"contract":{"protocolVersion":3,"requiredCapabilities":["catalog-selection"]}, + "sessionId":"catalog-session","selectionRef":"selection-1","outcome":"selected"} + """, + (server, session) -> server.catalog.select(new CatalogSelectParams( + new CatalogClientContract(3L, List.of("catalog-selection")), "catalog-session", + "selection-1", CatalogSelectionDecision.SELECTED))), + dispatch("session.managedSettings.get", SessionManagedSettingsGetResult.class, """ + {"sessionId":"bound-session"} + """, (server, session) -> session.managedSettings.get()), + dispatch("session.instructions.reload", SessionInstructionsGetSourcesResult.class, """ + {"sessionId":"bound-session"} + """, (server, session) -> session.instructions.reload()), + dispatch("session.customizations.reload", CustomizationsReloadResult.class, """ + {"sessionId":"bound-session"} + """, (server, session) -> session.customizations.reload()), + dispatch("session.plugins.install", SessionPluginsInstallResult.class, """ + {"sessionId":"bound-session","source":"plugin@market"} + """, + (server, session) -> session.plugins + .install(new SessionPluginsInstallParams("foreign-session", "plugin@market"))), + dispatch("session.plugins.uninstall", Void.class, """ + {"sessionId":"bound-session","name":"plugin","directSourceId":"source-1"} + """, + (server, session) -> session.plugins.uninstall( + new SessionPluginsUninstallParams("foreign-session", "plugin", "source-1"))), + dispatch("session.plugins.update", SessionPluginsUpdateResult.class, """ + {"sessionId":"bound-session","name":"plugin@market"} + """, + (server, session) -> session.plugins + .update(new SessionPluginsUpdateParams("foreign-session", "plugin@market"))), + dispatch("session.plugins.enable", Void.class, """ + {"sessionId":"bound-session","names":["first@market","second@market"]} + """, + (server, session) -> session.plugins.enable(new SessionPluginsEnableParams( + "foreign-session", List.of("first@market", "second@market")))), + dispatch("session.plugins.disable", Void.class, """ + {"sessionId":"bound-session","names":["plugin@market"]} + """, + (server, session) -> session.plugins.disable( + new SessionPluginsDisableParams("foreign-session", List.of("plugin@market")))), + dispatch("session.plugins.marketplaces.list", SessionPluginsMarketplacesListResult.class, """ + {"sessionId":"bound-session"} + """, (server, session) -> session.plugins.marketplaces.list()), + dispatch("session.plugins.marketplaces.add", SessionPluginsMarketplacesAddResult.class, """ + {"sessionId":"bound-session","source":"./market","workingDirectory":"/workspace"} + """, (server, session) -> session.plugins.marketplaces.add( new SessionPluginsMarketplacesAddParams("foreign-session", "./market", "/workspace"))), - dispatch("session.plugins.marketplaces.remove", SessionPluginsMarketplacesRemoveResult.class, """ - {"sessionId":"bound-session","name":"market","force":true} - """, - (server, session) -> session.plugins.marketplaces - .remove(new SessionPluginsMarketplacesRemoveParams("foreign-session", "market", true))), - dispatch("session.plugins.marketplaces.browse", SessionPluginsMarketplacesBrowseResult.class, """ - {"sessionId":"bound-session","name":"market"} - """, - (server, session) -> session.plugins.marketplaces - .browse(new SessionPluginsMarketplacesBrowseParams("foreign-session", "market"))), - dispatch("session.plugins.marketplaces.refresh", SessionPluginsMarketplacesRefreshResult.class, """ - {"sessionId":"bound-session","name":"market"} - """, - (server, session) -> session.plugins.marketplaces - .refresh(new SessionPluginsMarketplacesRefreshParams("foreign-session", "market"))), - dispatch("session.plugins.marketplaces.refresh", SessionPluginsMarketplacesRefreshResult.class, """ - {"sessionId":"bound-session"} - """, (server, session) -> session.plugins.marketplaces.refresh(null)), - dispatch("session.queue.withdrawMessage", SessionQueueWithdrawMessageResult.class, """ - {"sessionId":"bound-session","messageId":"message-1","expectedPrompt":"original"} - """, - (server, session) -> session.queue.withdrawMessage( - new SessionQueueWithdrawMessageParams("foreign-session", "message-1", "original"))), - dispatch("session.queue.appendSteering", SessionQueueAppendSteeringResult.class, """ - {"sessionId":"bound-session","messageId":"message-1","expectedPrompt":"original", - "agentMode":"interactive","prompt":"more","displayPrompt":"preview","attachments":[]} - """, (server, - session) -> session.queue.appendSteering(new SessionQueueAppendSteeringParams("foreign-session", - "message-1", "original", SendAgentMode.INTERACTIVE, "more", "preview", List.of()))), - dispatch("session.workflow.run", SessionWorkflowRunResult.class, """ - {"sessionId":"bound-session","name":"workflow-1","args":{"topic":"input"}} - """, - (server, session) -> session.workflow.run(new SessionWorkflowRunParams("foreign-session", - "workflow-1", Map.of("topic", "input"), null))), - dispatch("session.workflow.resume", SessionWorkflowResumeResult.class, """ - {"sessionId":"bound-session","runId":"run-1","notifyOnComplete":true,"logPhaseNames":false} - """, - (server, session) -> session.workflow.resume( - new SessionWorkflowResumeParams("foreign-session", "run-1", null, true, false))), - dispatch("session.workflow.runFromTool", WorkflowRunResult.class, """ - {"sessionId":"bound-session","name":"workflow-1","args":{"topic":"input"},"toolCallId":"tool-1"} - """, - (server, session) -> session.workflow.runFromTool(new SessionWorkflowRunFromToolParams( - "foreign-session", "workflow-1", Map.of("topic", "input"), null, "tool-1"))), - dispatch("session.workflow.resumeFromTool", WorkflowResumeResult.class, """ - {"sessionId":"bound-session","runId":"run-1","toolCallId":"tool-1"} - """, - (server, session) -> session.workflow.resumeFromTool( + dispatch("session.plugins.marketplaces.remove", SessionPluginsMarketplacesRemoveResult.class, + """ + {"sessionId":"bound-session","name":"market","force":true} + """, + (server, session) -> session.plugins.marketplaces.remove( + new SessionPluginsMarketplacesRemoveParams("foreign-session", "market", true))), + dispatch("session.plugins.marketplaces.browse", SessionPluginsMarketplacesBrowseResult.class, + """ + {"sessionId":"bound-session","name":"market"} + """, + (server, session) -> session.plugins.marketplaces.browse( + new SessionPluginsMarketplacesBrowseParams("foreign-session", "market"))), + dispatch("session.plugins.marketplaces.refresh", SessionPluginsMarketplacesRefreshResult.class, + """ + {"sessionId":"bound-session","name":"market"} + """, + (server, session) -> session.plugins.marketplaces.refresh( + new SessionPluginsMarketplacesRefreshParams("foreign-session", "market"))), + dispatch( + "session.plugins.marketplaces.refresh", SessionPluginsMarketplacesRefreshResult.class, + """ + {"sessionId":"bound-session"} + """, (server, session) -> session.plugins.marketplaces.refresh(null)), + dispatch("session.queue.withdrawMessage", SessionQueueWithdrawMessageResult.class, """ + {"sessionId":"bound-session","messageId":"message-1","expectedPrompt":"original"} + """, (server, + session) -> session.queue.withdrawMessage(new SessionQueueWithdrawMessageParams( + "foreign-session", "message-1", "original"))), + dispatch("session.queue.appendSteering", SessionQueueAppendSteeringResult.class, """ + {"sessionId":"bound-session","messageId":"message-1","expectedPrompt":"original", + "agentMode":"interactive","prompt":"more","displayPrompt":"preview","attachments":[]} + """, + (server, session) -> session.queue.appendSteering( + new SessionQueueAppendSteeringParams("foreign-session", "message-1", "original", + SendAgentMode.INTERACTIVE, "more", "preview", List.of()))), + dispatch("session.workflow.run", SessionWorkflowRunResult.class, """ + {"sessionId":"bound-session","name":"workflow-1","args":{"topic":"input"}} + """, + (server, session) -> session.workflow.run(new SessionWorkflowRunParams( + "foreign-session", "workflow-1", Map.of("topic", "input"), null))), + dispatch( + "session.workflow.resume", SessionWorkflowResumeResult.class, + """ + {"sessionId":"bound-session","runId":"run-1","notifyOnComplete":true,"logPhaseNames":false} + """, + (server, session) -> session.workflow.resume(new SessionWorkflowResumeParams( + "foreign-session", "run-1", null, true, false))), + dispatch( + "session.workflow.runFromTool", WorkflowRunResult.class, + """ + {"sessionId":"bound-session","name":"workflow-1","args":{"topic":"input"},"toolCallId":"tool-1"} + """, + (server, session) -> session.workflow.runFromTool(new SessionWorkflowRunFromToolParams( + "foreign-session", "workflow-1", Map.of("topic", "input"), null, "tool-1"))), + dispatch("session.workflow.resumeFromTool", WorkflowResumeResult.class, """ + {"sessionId":"bound-session","runId":"run-1","toolCallId":"tool-1"} + """, (server, session) -> session.workflow.resumeFromTool( new SessionWorkflowResumeFromToolParams("foreign-session", "run-1", null, "tool-1"))), - dispatch("session.workflow.getRun", SessionWorkflowGetRunResult.class, """ - {"sessionId":"bound-session","runId":"run-1"} - """, - (server, session) -> session.workflow - .getRun(new SessionWorkflowGetRunParams("foreign-session", "run-1"))), - dispatch("session.workflow.listRuns", SessionWorkflowListRunsResult.class, """ - {"sessionId":"bound-session","afterSeq":2,"limit":5} - """, - (server, session) -> session.workflow - .listRuns(new SessionWorkflowListRunsParams("foreign-session", 2L, null, 5L))), - dispatch("session.workflow.getRunDetail", SessionWorkflowGetRunDetailResult.class, """ - {"sessionId":"bound-session","runId":"run-1"} - """, - (server, session) -> session.workflow - .getRunDetail(new SessionWorkflowGetRunDetailParams("foreign-session", "run-1"))), - dispatch("session.workflow.getRunProgress", SessionWorkflowGetRunProgressResult.class, """ - {"sessionId":"bound-session","runId":"run-1","phaseId":"phase-1","afterSeq":2,"limit":5} - """, - (server, session) -> session.workflow.getRunProgress(new SessionWorkflowGetRunProgressParams( - "foreign-session", "run-1", "phase-1", 2L, null, 5L))), - dispatch("session.workflow.cancel", SessionWorkflowCancelResult.class, """ - {"sessionId":"bound-session","runId":"run-1"} - """, - (server, session) -> session.workflow - .cancel(new SessionWorkflowCancelParams("foreign-session", "run-1"))), - dispatch("session.workflow.pause", SessionWorkflowPauseResult.class, """ - {"sessionId":"bound-session","runId":"run-1"} - """, - (server, session) -> session.workflow - .pause(new SessionWorkflowPauseParams("foreign-session", "run-1"))), - dispatch("session.workflow.pauseAtCheckpoint", WorkflowPauseCheckpointResult.class, - """ - {"sessionId":"bound-session","runId":"run-1","executionToken":"execution-1","key":"checkpoint-1"} + dispatch("session.workflow.getRun", SessionWorkflowGetRunResult.class, """ + {"sessionId":"bound-session","runId":"run-1"} + """, + (server, session) -> session.workflow + .getRun(new SessionWorkflowGetRunParams("foreign-session", "run-1"))), + dispatch("session.workflow.listRuns", SessionWorkflowListRunsResult.class, """ + {"sessionId":"bound-session","afterSeq":2,"limit":5} + """, + (server, session) -> session.workflow + .listRuns(new SessionWorkflowListRunsParams("foreign-session", 2L, null, 5L))), + dispatch("session.workflow.getRunDetail", SessionWorkflowGetRunDetailResult.class, """ + {"sessionId":"bound-session","runId":"run-1"} """, - (server, session) -> session.workflow - .pauseAtCheckpoint(new SessionWorkflowPauseAtCheckpointParams("foreign-session", - "run-1", "execution-1", "checkpoint-1"))), - dispatch("session.workflow.log", Void.class, """ - {"sessionId":"bound-session","runId":"run-1","executionToken":"execution-1", - "lines":[{"seq":1,"kind":"log","text":"progress"}]} - """, (server, - session) -> session.workflow.log(new SessionWorkflowLogParams("foreign-session", "run-1", - "execution-1", List.of(new WorkflowLogLine(1L, WorkflowLogLineKind.LOG, "progress"))))), - dispatch("session.workflow.agent", SessionWorkflowAgentResult.class, - """ - {"sessionId":"bound-session","workflowRunId":"run-1","executionToken":"execution-1","prompt":"inspect"} + (server, session) -> session.workflow.getRunDetail( + new SessionWorkflowGetRunDetailParams("foreign-session", "run-1"))), + dispatch("session.workflow.getRunProgress", SessionWorkflowGetRunProgressResult.class, """ + {"sessionId":"bound-session","runId":"run-1","phaseId":"phase-1","afterSeq":2,"limit":5} + """, (server, + session) -> session.workflow.getRunProgress(new SessionWorkflowGetRunProgressParams( + "foreign-session", "run-1", "phase-1", 2L, null, 5L))), + dispatch("session.workflow.cancel", SessionWorkflowCancelResult.class, """ + {"sessionId":"bound-session","runId":"run-1"} """, - (server, session) -> session.workflow.agent(new SessionWorkflowAgentParams("foreign-session", - "run-1", "execution-1", "inspect", null))), - dispatch("session.workflow.journal.get", SessionWorkflowJournalGetResult.class, """ - {"sessionId":"bound-session","runId":"run-1","executionToken":"execution-1","key":"journal-1"} - """, - (server, session) -> session.workflow.journal.get(new SessionWorkflowJournalGetParams( - "foreign-session", "run-1", "execution-1", "journal-1"))), - dispatch("session.workflow.journal.put", Void.class, """ - {"sessionId":"bound-session","runId":"run-1","executionToken":"execution-1","key":"journal-1", - "resultJson":{"answer":42}} - """, (server, session) -> session.workflow.journal.put(new SessionWorkflowJournalPutParams( - "foreign-session", "run-1", "execution-1", "journal-1", Map.of("answer", 42))))); + (server, session) -> session.workflow + .cancel(new SessionWorkflowCancelParams("foreign-session", "run-1"))), + dispatch("session.workflow.pause", SessionWorkflowPauseResult.class, """ + {"sessionId":"bound-session","runId":"run-1"} + """, + (server, session) -> session.workflow + .pause(new SessionWorkflowPauseParams("foreign-session", "run-1"))), + dispatch( + "session.workflow.pauseAtCheckpoint", WorkflowPauseCheckpointResult.class, + """ + {"sessionId":"bound-session","runId":"run-1","executionToken":"execution-1","key":"checkpoint-1"} + """, + (server, session) -> session.workflow + .pauseAtCheckpoint(new SessionWorkflowPauseAtCheckpointParams("foreign-session", + "run-1", "execution-1", "checkpoint-1"))), + dispatch("session.workflow.log", Void.class, """ + {"sessionId":"bound-session","runId":"run-1","executionToken":"execution-1", + "lines":[{"seq":1,"kind":"log","text":"progress"}]} + """, + (server, session) -> session.workflow.log(new SessionWorkflowLogParams( + "foreign-session", "run-1", "execution-1", + List.of(new WorkflowLogLine(1L, WorkflowLogLineKind.LOG, "progress"))))), + dispatch("session.workflow.agent", SessionWorkflowAgentResult.class, + """ + {"sessionId":"bound-session","workflowRunId":"run-1","executionToken":"execution-1","prompt":"inspect"} + """, + (server, session) -> session.workflow.agent(new SessionWorkflowAgentParams( + "foreign-session", "run-1", "execution-1", "inspect", null))), + dispatch("session.workflow.journal.get", SessionWorkflowJournalGetResult.class, + """ + {"sessionId":"bound-session","runId":"run-1","executionToken":"execution-1","key":"journal-1"} + """, + (server, session) -> session.workflow.journal.get(new SessionWorkflowJournalGetParams( + "foreign-session", "run-1", "execution-1", "journal-1"))), + dispatch("session.workflow.journal.put", Void.class, + """ + {"sessionId":"bound-session","runId":"run-1","executionToken":"execution-1","key":"journal-1", + "resultJson":{"answer":42}} + """, + (server, session) -> session.workflow.journal + .put(new SessionWorkflowJournalPutParams("foreign-session", "run-1", + "execution-1", "journal-1", Map.of("answer", 42))))); } private static Arguments dispatch(String method, Class resultType, String expectedParams, @@ -551,8 +571,9 @@ void sessionRpc_mcp_list_injects_sessionId() { assertEquals(1, stub.calls.size()); assertEquals("session.mcp.list", stub.calls.get(0).method()); - var params = (Map) stub.calls.get(0).params(); - assertEquals("sess-mcp", params.get("sessionId")); + var mapper = new ObjectMapper(); + assertEquals(mapper.valueToTree(Map.of("sessionId", "sess-mcp")), + mapper.valueToTree(stub.calls.get(0).params())); } @Test diff --git a/java/sdk/src/test/java/com/github/copilot/generated/rpc/GeneratedRpcRecordsCoverageTest.java b/java/sdk/src/test/java/com/github/copilot/generated/rpc/GeneratedRpcRecordsCoverageTest.java index dace015cff..b68100cf85 100644 --- a/java/sdk/src/test/java/com/github/copilot/generated/rpc/GeneratedRpcRecordsCoverageTest.java +++ b/java/sdk/src/test/java/com/github/copilot/generated/rpc/GeneratedRpcRecordsCoverageTest.java @@ -334,12 +334,6 @@ void sessionMcpEnableParams_record() { assertEquals("mcp-server-2", params.serverName()); } - @Test - void sessionMcpListParams_record() { - var params = new SessionMcpListParams("sess-27"); - assertEquals("sess-27", params.sessionId()); - } - @Test void sessionMcpReloadParams_record() { var params = new SessionMcpReloadParams("sess-28"); diff --git a/nodejs/package.json b/nodejs/package.json index e6b1e9992c..ab2ea01ff2 100644 --- a/nodejs/package.json +++ b/nodejs/package.json @@ -5,7 +5,7 @@ "url": "https://github.com/github/copilot-sdk.git" }, "version": "0.0.0-dev", - "copilotCliVersion": "1.0.92-4", + "copilotCliVersion": "1.0.92-5", "description": "TypeScript SDK for programmatic control of GitHub Copilot CLI via JSON-RPC", "main": "./dist/cjs/index.js", "types": "./dist/index.d.ts", diff --git a/nodejs/src/cliVersion.ts b/nodejs/src/cliVersion.ts index 93029c8524..6338d1e0d2 100644 --- a/nodejs/src/cliVersion.ts +++ b/nodejs/src/cliVersion.ts @@ -1,3 +1,3 @@ -export const COPILOT_CLI_VERSION = "1.0.92-4"; +export const COPILOT_CLI_VERSION = "1.0.92-5"; export const COPILOT_CLI_USE_NPM_PACKAGE = false; diff --git a/nodejs/src/generated/rpc.ts b/nodejs/src/generated/rpc.ts index b423be039f..4511e89f22 100644 --- a/nodejs/src/generated/rpc.ts +++ b/nodejs/src/generated/rpc.ts @@ -31,6 +31,7 @@ export type AuthInfo = | TokenProviderAuthInfo | CopilotApiTokenAuthInfo | UserAuthInfo + | AccountAuthInfo | GhCliAuthInfo | ApiKeyAuthInfo; /** @@ -416,6 +417,8 @@ export type AuthInfoType = | "env" /** Authentication from an interactive user sign-in. */ | "user" + /** Authentication from a selected provider-owned account, without a GitHub credential. */ + | "account" /** Authentication delegated to the GitHub CLI. */ | "gh-cli" /** Authentication from an API key credential. */ @@ -484,17 +487,19 @@ export type AuthLoginStep = kind: "error"; }; /** - * Terminal disposition of a login persistence attempt. + * Disposition of a login attempt, including pending user decisions. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema * via the `definition` "AuthLoginResultStatus". */ /** @experimental */ export type AuthLoginResultStatus = - /** The credential was persisted and the account is signed in. */ + /** The credential was persisted and the selected account is signed in. */ | "completed" /** Persistence needs explicit consent to store the token in plaintext. */ | "needs-plaintext-consent" + /** Credentials are saved; select an account using a returned selectionId as advance input to complete sign-in. */ + | "needs-account-selection" /** The user declined plaintext persistence. */ | "declined"; /** @@ -5029,6 +5034,7 @@ export type SettableAuthInfo = | SettableTokenAuthInfo | CopilotApiTokenAuthInfo | UserAuthInfo + | AccountAuthInfo | GhCliAuthInfo | ApiKeyAuthInfo; /** @@ -6773,6 +6779,27 @@ export interface UserAuthInfo { login: string; copilotUser?: CopilotUserResponse; } +/** + * An interactive account whose model provider owns its credentials. It carries no GitHub credential. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "AccountAuthInfo". + */ +/** @experimental */ +export interface AccountAuthInfo { + /** + * Provider-owned account authentication. + */ + type: "account"; + /** + * Host coordinate owned by the account's model provider. + */ + host: string; + /** + * Login identifying the provider-owned account. + */ + login: string; +} /** * Authentication-info input variant for GitHub CLI credentials, carrying host, login, and the `gh auth token` value. * @@ -7496,6 +7523,28 @@ export interface AuthIdentityMetadata { */ login: string; } +/** + * A credential-free account choice after sign-in. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "AuthLoginAccount". + */ +/** @experimental */ +export interface AuthLoginAccount { + /** + * Opaque identifier supplied to the next login step to select this account. + */ + selectionId: string; + /** + * Host coordinate owned by the selected account's provider. + */ + host: string; + /** + * Human-readable login for the account choice. + */ + login: string; + kind: AccountKind; +} /** * Advance an in-flight login flow, optionally fulfilling an input-required step. * @@ -7538,7 +7587,7 @@ export interface AuthLoginBegun { step: AuthLoginStep; } /** - * Terminal result of an interactive login flow. + * Result of an interactive login flow. Pending consent or account selection is not terminal. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema * via the `definition` "AuthLoginResultDto". @@ -7554,6 +7603,10 @@ export interface AuthLoginResultDto { * Login that was signed in, when completed. */ login?: string; + /** + * Available accounts when sign-in is awaiting account selection, ordered with Microsoft 365 first. + */ + accounts?: AuthLoginAccount[]; } /** * Cancel an in-flight login flow. @@ -14775,6 +14828,64 @@ export interface McpConfigUpdateRequest { name: string; config: McpSerializableServerConfig; } +/** + * Effective MCP configuration entry. Configuration enablement is distinct from the optional live observation. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "McpConfiguredServer". + */ +/** @experimental */ +export interface McpConfiguredServer { + /** + * Server name (config key) + */ + name: string; + /** + * Whether this configured server is enabled after session configuration and policy filtering. + */ + enabled: boolean; + source?: McpServerSource; + /** + * Plugin name that provided this server, when source is plugin. + */ + sourcePlugin?: string; + /** + * Plugin version that provided this server, when source is plugin. + */ + sourcePluginVersion?: string; + /** + * Human-readable display name supplied by configuration. + */ + displayName?: string; + live?: McpConfiguredServerState; +} +/** + * Observational state for a matching already materialized MCP server. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "McpConfiguredServerState". + */ +/** @experimental */ +export interface McpConfiguredServerState { + status: McpServerStatus; + /** + * Observed connection error, when the materialized server failed. + */ + error?: string; +} +/** + * Effective MCP configuration with optional live observations from matching already materialized servers. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "McpConfiguredServerList". + */ +/** @experimental */ +export interface McpConfiguredServerList { + /** + * Effective configured MCP servers. + */ + servers: McpConfiguredServer[]; +} /** * Credential-free authentication identity used to configure GitHub MCP. * @@ -32682,12 +32793,19 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin /** @experimental */ mcp: { /** - * Lists MCP servers configured for the session, their connection status, and host-level state. The host-level state (disabled/filtered servers, failed/needs-auth/pending connections, mcp3p policy, full config) is empty/zero when no MCP host has been initialized for the session. + * Lists materialized MCP servers and their connection status. Cache misses may start and wait for MCP servers. * * @returns MCP servers configured for the session, with their connection status and host-level state. */ list: async (): Promise => connection.sendRequest("session.mcp.list", { sessionId }), + /** + * Lists effective MCP configuration without starting, restarting, authenticating, or waiting for servers. An optional live observation is from an already materialized matching server; this is not a readiness guarantee. + * + * @returns Effective MCP configuration with optional live observations from matching already materialized servers. + */ + listConfigured: async (): Promise => + connection.sendRequest("session.mcp.listConfigured", { sessionId }), /** * Lists the tools exposed by a connected MCP server on this session's host. This performs a live `tools/list` request. Tool UI metadata is returned independently of whether MCP Apps rendering is enabled for the session. * diff --git a/nodejs/src/generated/session-events.ts b/nodejs/src/generated/session-events.ts index d6e4a099d9..9146818e4c 100644 --- a/nodejs/src/generated/session-events.ts +++ b/nodejs/src/generated/session-events.ts @@ -1618,7 +1618,7 @@ export type McpServerStatus = | "pending" /** The server is configured but disabled. */ | "disabled" - /** The server was intentionally stopped and can be restarted on demand when policy permits; a server quarantined by restrictive managed policy stays stopped and cannot be restarted until the policy allows it. */ + /** The server is not running: it may not have started yet, may have been explicitly stopped, or may be quarantined by restrictive managed policy. It can be restarted on demand when policy permits. */ | "stopped" /** The server is not configured for this session. */ | "not_configured"; diff --git a/nodejs/test/csharp-codegen.test.ts b/nodejs/test/csharp-codegen.test.ts index 1ad071369a..8ff9e3142c 100644 --- a/nodejs/test/csharp-codegen.test.ts +++ b/nodejs/test/csharp-codegen.test.ts @@ -300,6 +300,25 @@ describe("C# session event dispatch", () => { }); describe("C# RPC codegen", () => { + it("emits parameterless MCP list methods", () => { + const code = generateRpcCode({ + session: { + mcp: { + list: { + rpcMethod: "session.mcp.list", + params: null, + }, + }, + }, + }); + expect(code).toContain( + "public async Task ListAsync(CancellationToken cancellationToken = default)" + ); + expect(code).not.toContain("ListWithParamsAsync"); + expect(code).not.toContain("StartServers"); + expect(code).toContain('"session.mcp.list"'); + }); + it("preserves arbitrary JSON handoff settings instead of emitting an empty DTO", () => { const code = generateRpcCode({ server: { diff --git a/nodejs/test/e2e/disabled_mcp_servers.e2e.test.ts b/nodejs/test/e2e/disabled_mcp_servers.e2e.test.ts index d9e8cee28c..f771e54dee 100644 --- a/nodejs/test/e2e/disabled_mcp_servers.e2e.test.ts +++ b/nodejs/test/e2e/disabled_mcp_servers.e2e.test.ts @@ -223,7 +223,10 @@ describe("disabled MCP servers", async () => { }, }); - function createPluginDirectory(prefix: string): { + function createPluginDirectory( + prefix: string, + controlArgs: string[] = [] + ): { pluginDirectory: string; controlMarker: string; disabledMarker: string; @@ -253,6 +256,7 @@ describe("disabled MCP servers", async () => { controlMarker, "--server-name", "control", + ...controlArgs, ], }, disabled: { @@ -339,6 +343,194 @@ describe("disabled MCP servers", async () => { ); } + it.each(["connected", "failed"] as const)( + "lists cold configured servers without starting or awaiting them before a %s first-turn startup", + { timeout: 120_000 }, + async (expectedStatus) => { + const startupGate = join(workDir, `startup-gate-${randomUUID()}`); + const { pluginDirectory, controlMarker, disabledMarker } = createPluginDirectory( + "configured-mcp", + [ + "--startup-gate", + startupGate, + ...(expectedStatus === "failed" ? ["--fail-startup"] : []), + ] + ); + await using session = await client.createSession({ + onPermissionRequest: approveAll, + pluginDirectories: [pluginDirectory], + disabledMcpServers: ["disabled"], + }); + + for (let i = 0; i < 2; i++) { + const inventory = await session.rpc.mcp.listConfigured(); + expect( + inventory.servers + .filter((server) => ["control", "disabled"].includes(server.name)) + .map(({ name, enabled, live }) => ({ name, enabled, live })) + .sort((a, b) => a.name.localeCompare(b.name)) + ).toEqual([ + { name: "control", enabled: true, live: undefined }, + { name: "disabled", enabled: false, live: undefined }, + ]); + expect(markerCount(controlMarker)).toBe(0); + expect(markerCount(disabledMarker)).toBe(0); + } + + const [response] = await Promise.all([ + session.sendAndWait({ prompt: MCP_TRIGGER_PROMPT }, 90_000), + (async () => { + try { + await waitForMarkerCount(controlMarker, 1); + // Inventory must also return while initialize is held behind the gate. + const inventory = await session.rpc.mcp.listConfigured(); + expect( + inventory.servers.find((server) => server.name === "control")?.live + ?.status + ).toBe("pending"); + expect(markerCount(controlMarker)).toBe(1); + } finally { + writeFileSync(startupGate, "release"); + } + })(), + ]); + expectSyntheticResponse(response); + const startsAfterTurn = markerCount(controlMarker); + if (expectedStatus === "connected") { + expect(startsAfterTurn).toBe(1); + } + // Turn initialization may retry a failed server; inventory must not. + for (let i = 0; i < 2; i++) { + const inventory = await session.rpc.mcp.listConfigured(); + expect( + inventory.servers.find((server) => server.name === "control")?.live?.status + ).toBe(expectedStatus); + expect(markerCount(controlMarker)).toBe(startsAfterTurn); + } + expect(markerCount(disabledMarker)).toBe(0); + } + ); + + // SDK runners provide a sandbox backend only on macOS (no bwrap/BaseContainer elsewhere). + it.skipIf(process.platform !== "darwin").each([ + ["create", "connected"], + ["create", "failed"], + ["resume", "connected"], + ["resume", "failed"], + ] as const)( + "%s applies sandbox without starting MCP and skips a gated %s startup", + { timeout: 120_000 }, + async (mode, expectedStatus) => { + const startupGate = join(workDir, `sandbox-startup-gate-${randomUUID()}`); + const { pluginDirectory, controlMarker, disabledMarker } = createPluginDirectory( + "sandbox-mcp", + [ + "--startup-gate", + startupGate, + ...(expectedStatus === "failed" ? ["--fail-startup"] : []), + ] + ); + const createOptions = { + onPermissionRequest: approveAll, + enableSessionStore: true, + pluginDirectories: [pluginDirectory], + disabledMcpServers: ["disabled"], + }; + const activeClient = createClient({ + connection: RuntimeConnection.forStdio({ path: process.env.COPILOT_CLI_PATH }), + requestHandler: new PersistingRequestHandler(), + }); + let sessionId: string | undefined; + if (mode === "resume") { + const seedClient = createClient({ + connection: RuntimeConnection.forStdio({ path: process.env.COPILOT_CLI_PATH }), + requestHandler: new PersistingRequestHandler(), + }); + try { + const seed = await seedClient.createSession({ + onPermissionRequest: approveAll, + enableSessionStore: true, + }); + sessionId = seed.sessionId; + expectSyntheticResponse(await seed.sendAndWait({ prompt: MCP_TRIGGER_PROMPT })); + } finally { + await seedClient.stop(); + } + } + try { + await using session = sessionId + ? await activeClient.resumeSession(sessionId, createOptions) + : await activeClient.createSession(createOptions); + let startupFailure: string | undefined; + const unsubscribe = session.on("session.mcp_server_status_changed", (event) => { + if (event.data.serverName === "control" && event.data.status === "failed") { + startupFailure = event.data.error ?? "MCP startup failed"; + } + }); + try { + const updated = await session.rpc.options.update({ + sandboxConfig: { + enabled: true, + allowBypass: true, + addCurrentWorkingDirectory: true, + allowDevToolAccess: true, + userPolicy: { + // The fixture and its dependencies live outside the session workspace. + filesystem: { + readonlyPaths: [resolve(__dirname, "../../../../..")], + }, + }, + }, + }); + expect(updated.success).toBe(true); + const inventory = await session.rpc.mcp.listConfigured(); + expect(inventory.servers.filter((s) => s.name === "control")).toMatchObject([ + { enabled: true }, + ]); + expect(markerCount(controlMarker)).toBe(0); + expect(markerCount(disabledMarker)).toBe(0); + const [response] = await Promise.all([ + session.sendAndWait({ prompt: MCP_TRIGGER_PROMPT }, 60_000), + (async () => { + await waitForCondition( + () => { + if (startupFailure) { + throw new Error(startupFailure); + } + return markerCount(controlMarker) === 1; + }, + { timeoutMs: 60_000, intervalMs: 100 } + ); + const background = await session.rpc.mcp.moveLoadingToBackground(); + expect(background.movedToBackground).toBe(true); + })(), + ]); + expectSyntheticResponse(response); + expect(existsSync(startupGate)).toBe(false); + expect(markerCount(controlMarker)).toBe(1); + expect(markerCount(disabledMarker)).toBe(0); + writeFileSync(startupGate, "release"); + await waitForCondition( + async () => { + const configured = await session.rpc.mcp.listConfigured(); + return ( + configured.servers.find((server) => server.name === "control")?.live + ?.status === expectedStatus + ); + }, + { timeoutMs: 60_000, intervalMs: 100 } + ); + expect(markerCount(controlMarker)).toBe(1); + } finally { + unsubscribe(); + writeFileSync(startupGate, "release"); + } + } finally { + await activeClient.stop(); + } + } + ); + it( "keeps disabled plugin MCP servers per-session on create", { timeout: 120_000 }, diff --git a/nodejs/test/e2e/extension_env_access.e2e.test.ts b/nodejs/test/e2e/extension_env_access.e2e.test.ts index 15da3a9327..236201a21b 100644 --- a/nodejs/test/e2e/extension_env_access.e2e.test.ts +++ b/nodejs/test/e2e/extension_env_access.e2e.test.ts @@ -103,8 +103,9 @@ async function runExtensionAgainstStubHost(options: { await retry( "wait for the fixture extension to report its join result", async () => { + // File creation is visible before the child writes the result. expect( - existsSync(resultFile), + existsSync(resultFile) && readFileSync(resultFile, "utf-8") !== "", `extension never reported; stderr: ${stderr.join("")}` ).toBe(true); }, diff --git a/nodejs/test/e2e/harness/sdkTestContext.ts b/nodejs/test/e2e/harness/sdkTestContext.ts index 4a54acca93..fd755c0289 100644 --- a/nodejs/test/e2e/harness/sdkTestContext.ts +++ b/nodejs/test/e2e/harness/sdkTestContext.ts @@ -3,7 +3,8 @@ *--------------------------------------------------------------------------------------------*/ import fs, { realpathSync } from "fs"; -import { rm } from "fs/promises"; +import { readdir, rm } from "fs/promises"; +import { createInterface } from "node:readline"; import os from "os"; import { basename, dirname, join, resolve } from "path"; import { rimraf } from "rimraf"; @@ -259,8 +260,31 @@ export async function createSdkTestContext({ // Wire up to Vitest lifecycle beforeEach(async (testContext) => { // Must be inside beforeEach - vitest requires test context - onTestFailed(() => { + onTestFailed(async () => { anyTestFailed = true; + // Preserve only the bounded, metadata-only runtime diagnostics before afterAll removes the home. + if (logLevel === "debug") { + try { + const logDir = join(copilotHomeDir, "logs"); + for (const file of await readdir(logDir)) { + if (!file.endsWith(".log")) continue; + const lines = createInterface({ + input: fs.createReadStream(join(logDir, file)), + }); + const diagnostics: string[] = []; + for await (const line of lines) { + if (!/^\S+ \[DEBUG\] \[rust:sdk_diagnostics\] /.test(line)) continue; + diagnostics.push(line); + if (diagnostics.length > 200) diagnostics.shift(); + } + console.error( + `[SDK runtime diagnostics: ${file}; last ${diagnostics.length} matching lines]\n${diagnostics.join("\n")}` + ); + } + } catch (error) { + console.error(`Could not read SDK runtime diagnostics: ${formatError(error)}`); + } + } }); // Finished hooks run in reverse order, so test-owned clients stop before files are removed. onTestFinished(async () => { diff --git a/nodejs/test/e2e/mcp_instruction_recovery.e2e.test.ts b/nodejs/test/e2e/mcp_instruction_recovery.e2e.test.ts index 9861ad13e9..299f173e4f 100644 --- a/nodejs/test/e2e/mcp_instruction_recovery.e2e.test.ts +++ b/nodejs/test/e2e/mcp_instruction_recovery.e2e.test.ts @@ -101,14 +101,14 @@ describe("MCP catalog recovery", async () => { events.some( (event) => event.type === "tool.execution_complete" && - event.data.success && - event.data.result?.content.includes("CATALOG_RECORD") + !event.data.success && + event.data.error?.message.includes( + "MCP tool catalog changed before tool" + ) ), - "the reconnected server must execute the offered tool" + "the first stale call must be refused before dispatch" ).toBe(true); - expect(changing.calls, "only one call may reach the reconnected MCP server").toBe( - 1 - ); + expect(changing.calls, "only the recovered call may reach the MCP server").toBe(1); } const exchanges = await openAiEndpoint.getExchanges(); @@ -121,7 +121,7 @@ describe("MCP catalog recovery", async () => { (message) => message.role === "tool" && JSON.stringify(message.content).includes( - staleCall ? "CATALOG_RECORD" : RECONNECT_RESULT + staleCall ? "MCP tool catalog changed before tool" : RECONNECT_RESULT ) ) ); diff --git a/nodejs/test/e2e/mcp_oauth.e2e.test.ts b/nodejs/test/e2e/mcp_oauth.e2e.test.ts index 0ec14a5ada..8ce68ee70f 100644 --- a/nodejs/test/e2e/mcp_oauth.e2e.test.ts +++ b/nodejs/test/e2e/mcp_oauth.e2e.test.ts @@ -117,6 +117,16 @@ describe("MCP OAuth host auth", async () => { expect(tokenForm.get("redirect_uri")).toBe(redirectUri); expect(tokenForm.get("client_id")).toBe("sdk-e2e-client"); expect(tokenForm.get("code_verifier")).toEqual(expect.any(String)); + // This server issues neither a refresh token nor an account pin. + await expect( + session.rpc.mcp.oauth.authenticationStateChanged({ + serverName, + refreshSessionToken: true, + }) + ).rejects.toThrow("no refresh token or eligible saved Entra account"); + const afterRefresh = await oauthServer.requests(); + expect(afterRefresh.filter((request) => request.path === "/token")).toHaveLength(1); + expect(afterRefresh.filter((request) => request.path === "/register")).toHaveLength(0); expect( requests.some((request) => request.authorization === `Bearer ${EXPECTED_TOKEN}`) ).toBe(true); diff --git a/nodejs/test/e2e/rpc_surface_coverage.e2e.test.ts b/nodejs/test/e2e/rpc_surface_coverage.e2e.test.ts index 2a8f34a7d2..4995e08094 100644 --- a/nodejs/test/e2e/rpc_surface_coverage.e2e.test.ts +++ b/nodejs/test/e2e/rpc_surface_coverage.e2e.test.ts @@ -327,7 +327,7 @@ describe("Generated RPC surface coverage", () => { ...collectRuntimeFunctions(session.rpc, "session"), ]); - expect(inventory).toHaveLength(394); + expect(inventory).toHaveLength(395); const sessionProviderMethods = [ "providers.getCatalog", "providers.discover", diff --git a/nodejs/test/e2e/telemetry_http.e2e.test.ts b/nodejs/test/e2e/telemetry_http.e2e.test.ts new file mode 100644 index 0000000000..7ffc613899 --- /dev/null +++ b/nodejs/test/e2e/telemetry_http.e2e.test.ts @@ -0,0 +1,257 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +import { randomUUID } from "node:crypto"; +import { createServer } from "node:http"; +import type { Server } from "node:http"; +import { join } from "node:path"; +import { describe, it } from "vitest"; +import { z } from "zod"; +import { approveAll, defineTool, RuntimeConnection } from "../../src/index.js"; +import type { CopilotClient } from "../../src/index.js"; +import { createSdkTestContext } from "./harness/sdkTestContext.js"; + +const attribute = z.object({ + key: z.string(), + value: z.object({ stringValue: z.string().optional() }).passthrough(), +}); +const tracePayload = z.object({ + resourceSpans: z.array( + z.object({ + scopeSpans: z.array( + z.object({ + scope: z.object({ name: z.string() }), + spans: z.array( + z + .object({ + traceId: z.string(), + spanId: z.string(), + attributes: z.array(attribute), + }) + .passthrough() + ), + }) + ), + }) + ), +}); +const metricPayload = z.object({ + resourceMetrics: z.array( + z.object({ + scopeMetrics: z.array( + z.object({ + scope: z.object({ name: z.string() }), + metrics: z.array(z.object({ name: z.string() }).passthrough()), + }) + ), + }) + ), +}); +const exportRecord = z.object({ path: z.string(), payload: z.unknown() }); +type ExportRecord = z.infer; + +async function closeServer(server: Server): Promise { + server.closeAllConnections(); + await new Promise((resolve, reject) => + server.close((error) => (error ? reject(error) : resolve())) + ); +} + +describe("HTTP telemetry export", async () => { + // The KDC workflow runs this same public SDK scenario through real Squid + // authentication. Ordinary SDK CI uses a test-owned loopback collector. + const proxy = process.env.COPILOT_KDC_PROXY_URL; + const collectorUrl = process.env.COPILOT_KDC_COLLECTOR_URL; + const ca = process.env.COPILOT_KDC_CA_PATH; + if (proxy && (!collectorUrl || !ca)) { + throw new Error("KDC telemetry E2E requires collector URL and CA path"); + } + const caseId = randomUUID(); + const harness = await createSdkTestContext({ + copilotClientOptions: { + connection: RuntimeConnection.forStdio(), + env: { + OTEL_EXPORTER_OTLP_TRACES_ENDPOINT: "", + OTEL_EXPORTER_OTLP_METRICS_ENDPOINT: "", + OTEL_EXPORTER_OTLP_TRACES_PROTOCOL: "", + OTEL_EXPORTER_OTLP_METRICS_PROTOCOL: "", + OTEL_EXPORTER_OTLP_CERTIFICATE: "", + OTEL_EXPORTER_OTLP_TRACES_CERTIFICATE: "", + OTEL_EXPORTER_OTLP_METRICS_CERTIFICATE: "", + OTEL_EXPORTER_OTLP_CLIENT_CERTIFICATE: "", + OTEL_EXPORTER_OTLP_TRACES_CLIENT_CERTIFICATE: "", + OTEL_EXPORTER_OTLP_METRICS_CLIENT_CERTIFICATE: "", + OTEL_EXPORTER_OTLP_CLIENT_KEY: "", + OTEL_EXPORTER_OTLP_TRACES_CLIENT_KEY: "", + OTEL_EXPORTER_OTLP_METRICS_CLIENT_KEY: "", + OTEL_EXPORTER_OTLP_TIMEOUT: "10000", + OTEL_EXPORTER_OTLP_TRACES_TIMEOUT: "", + OTEL_EXPORTER_OTLP_METRICS_TIMEOUT: "", + OTEL_EXPORTER_OTLP_TRACES_HEADERS: `x-test-case=${caseId}`, + OTEL_EXPORTER_OTLP_METRICS_HEADERS: `x-test-case=${caseId}`, + OTEL_EXPORTER_OTLP_HEADERS: `x-test-case=${caseId}`, + NO_PROXY: "127.0.0.1,localhost,::1", + no_proxy: "127.0.0.1,localhost,::1", + ...(proxy + ? { + HTTP_PROXY: proxy, + http_proxy: proxy, + HTTPS_PROXY: proxy, + https_proxy: proxy, + NODE_EXTRA_CA_CERTS: ca, + SSL_CERT_FILE: ca, + CURL_CA_BUNDLE: ca, + COPILOT_PROXY_KERBEROS_SPN: "HTTP/proxy.example.test", + } + : {}), + }, + }, + }); + + it( + "exports completed SDK turns as OTLP traces and metrics", + { timeout: 90_000 }, + async ({ expect, onTestFinished }) => { + const received: ExportRecord[] = []; + const failures: Error[] = []; + let client: CopilotClient | undefined; + let startingServer: Promise | undefined; + onTestFinished(async () => { + try { + if (client) { + expect(await client.stop()).toEqual([]); + } + } finally { + const [startup] = await Promise.allSettled( + startingServer ? [startingServer] : [] + ); + if (startup?.status === "fulfilled") { + await closeServer(startup.value); + } + } + }); + let endpoint = "https://kerberos-proxy-test.invalid:8443"; + if (!proxy) { + const server = createServer((request, response) => { + void (async () => { + expect(request.headers["x-test-case"]).toBe(caseId); + const chunks: Buffer[] = []; + for await (const chunk of request) { + chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)); + } + received.push({ + path: request.url ?? "", + payload: JSON.parse(Buffer.concat(chunks).toString("utf8")), + }); + response.setHeader("content-type", "application/json"); + response.end("{}"); + })().catch((error: Error) => { + failures.push(error); + response.writeHead(500).end(); + }); + }); + startingServer = new Promise((resolve, reject) => { + server.once("error", reject); + server.listen(0, "127.0.0.1", () => resolve(server)); + }); + await startingServer; + const address = server.address(); + if (!address || typeof address === "string") { + throw new Error("OTLP collector did not bind a TCP address"); + } + endpoint = `http://127.0.0.1:${address.port}`; + } + + // Reuse the existing telemetry turn capture, unchanged: the transport + // test does not need another model sample or handcrafted response. + await harness.openAiEndpoint.updateConfig({ + filePath: join( + import.meta.dirname, + "..", + "..", + "..", + "test", + "snapshots", + "telemetry", + "should_export_file_telemetry_for_sdk_interactions.yaml" + ), + workDir: harness.workDir, + }); + const sourceName = "ts-sdk-telemetry-e2e"; + client = harness.createClient({ + telemetry: { + exporterType: "otlp-http", + otlpEndpoint: endpoint, + otlpProtocol: "http/json", + sourceName, + captureContent: true, + }, + }); + const session = await client.createSession({ + onPermissionRequest: approveAll, + tools: [ + defineTool("echo_telemetry_marker", { + description: "Echoes a marker string for telemetry validation.", + parameters: z.object({ value: z.string() }), + handler: ({ value }) => value, + }), + ], + }); + const assistant = await session.sendAndWait( + { + prompt: "Use the echo_telemetry_marker tool with value 'copilot-sdk-telemetry-e2e', then respond with TELEMETRY_E2E_DONE.", + }, + 90_000 + ); + expect(assistant?.data.content).toContain("TELEMETRY_E2E_DONE"); + await session.disconnect(); + expect(await client.stop()).toEqual([]); + + if (proxy) { + const response = await fetch(`${collectorUrl}/exports/${caseId}`); + expect(response.ok).toBe(true); + received.push(...z.array(exportRecord).parse(await response.json())); + } + expect(failures).toEqual([]); + const traces = received + .filter(({ path }) => path === "/v1/traces") + .flatMap(({ payload }) => tracePayload.parse(payload).resourceSpans) + .flatMap(({ scopeSpans }) => scopeSpans); + const metrics = received + .filter(({ path }) => path === "/v1/metrics") + .flatMap(({ payload }) => metricPayload.parse(payload).resourceMetrics) + .flatMap(({ scopeMetrics }) => scopeMetrics); + expect(traces.length).toBeGreaterThan(0); + expect(metrics.length).toBeGreaterThan(0); + expect(traces.every(({ scope }) => scope.name === sourceName)).toBe(true); + expect(metrics.every(({ scope }) => scope.name === sourceName)).toBe(true); + const spans = traces.flatMap(({ spans }) => spans); + expect( + spans.some(({ attributes }) => + attributes.some( + ({ key, value }) => + key === "gen_ai.conversation.id" && + value.stringValue === session.sessionId + ) + ) + ).toBe(true); + expect( + spans.some(({ attributes }) => + attributes.some( + ({ key, value }) => + key === "gen_ai.output.messages" && + value.stringValue?.includes("TELEMETRY_E2E_DONE") + ) + ) + ).toBe(true); + expect( + spans.every( + ({ traceId, spanId }) => + /^[0-9a-f]{32}$/i.test(traceId) && /^[0-9a-f]{16}$/i.test(spanId) + ) + ).toBe(true); + expect(metrics.flatMap(({ metrics }) => metrics).length).toBeGreaterThan(0); + } + ); +}); diff --git a/nodejs/test/e2e/workflow.e2e.test.ts b/nodejs/test/e2e/workflow.e2e.test.ts index 093654d79a..66718c35c2 100644 --- a/nodejs/test/e2e/workflow.e2e.test.ts +++ b/nodejs/test/e2e/workflow.e2e.test.ts @@ -23,9 +23,11 @@ const cliDistDirectory = process.env.COPILOT_EXTENSION_SDK_PATH ? dirname(process.env.COPILOT_EXTENSION_SDK_PATH) : dirname(cliPath); const workflowTestContext = await createSdkTestContext({ + logLevel: "debug", copilotClientOptions: { connection: RuntimeConnection.forStdio({ path: cliPath }), env: { + RUST_LOG: "error,sdk_diagnostics=debug", COPILOT_CLI_ENABLED_FEATURE_FLAGS: "EXTENSIONS", }, extensionLaunchProvider: { diff --git a/nodejs/test/mcp-list.test.ts b/nodejs/test/mcp-list.test.ts new file mode 100644 index 0000000000..8b19e799eb --- /dev/null +++ b/nodejs/test/mcp-list.test.ts @@ -0,0 +1,49 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +import { PassThrough } from "node:stream"; +import { createMessageConnection } from "vscode-jsonrpc/node.js"; +import { describe, expect, it, vi } from "vitest"; +import { createSessionRpc } from "../src/generated/rpc.js"; + +describe("session MCP wire contracts", () => { + it("uses parameterless list endpoints with the wrapper session", async () => { + const input = new PassThrough(); + const output = new PassThrough(); + const connection = createMessageConnection(input, output); + const sendRequest = vi.spyOn(connection, "sendRequest").mockResolvedValue({ servers: [] }); + try { + const mcp = createSessionRpc(connection, "wrapper-session").mcp; + await mcp.list(); + await mcp.listConfigured(); + expect(sendRequest.mock.calls).toEqual([ + ["session.mcp.list", { sessionId: "wrapper-session" }], + ["session.mcp.listConfigured", { sessionId: "wrapper-session" }], + ]); + } finally { + connection.dispose(); + input.destroy(); + output.destroy(); + } + }); + + it("keeps the wrapper session authoritative over a supplied sessionId", async () => { + const input = new PassThrough(); + const output = new PassThrough(); + const connection = createMessageConnection(input, output); + const sendRequest = vi.spyOn(connection, "sendRequest").mockResolvedValue(undefined); + try { + const mcp = createSessionRpc(connection, "wrapper-session").mcp; + const params = { serverName: "test-server", sessionId: "forged-session" }; + await mcp.enable(params); + expect(sendRequest.mock.calls).toEqual([ + ["session.mcp.enable", { serverName: "test-server", sessionId: "wrapper-session" }], + ]); + } finally { + connection.dispose(); + input.destroy(); + output.destroy(); + } + }); +}); diff --git a/nodejs/test/python-codegen.test.ts b/nodejs/test/python-codegen.test.ts index 2fdc6ffa3a..f189e9619d 100644 --- a/nodejs/test/python-codegen.test.ts +++ b/nodejs/test/python-codegen.test.ts @@ -271,6 +271,23 @@ describe("Python append-last fields for a selected schema", () => { ).toEqual(fields); }); + it("includes fields marked append-last by the contract schema", () => { + expect( + pythonAppendLastFieldsPresentIn({ + Response: { + type: "object", + properties: { + stableField: { type: "string" }, + addedField: { + type: "string", + "x-copilot-sdk-append-last": true, + }, + }, + }, + }) + ).toContainEqual(["Response", "addedField"]); + }); + it("skips an entry for a legacy request or a schema without the definition", () => { expect( pythonAppendLastFieldsPresentIn( diff --git a/nodejs/test/rust-codegen.test.ts b/nodejs/test/rust-codegen.test.ts index 17fc4eea47..0a169c0825 100644 --- a/nodejs/test/rust-codegen.test.ts +++ b/nodejs/test/rust-codegen.test.ts @@ -17,6 +17,21 @@ import { import { legacyRequestSchema } from "./legacy-parameters-fixture.ts"; describe("Rust API type codegen", () => { + it("does not emit MCP list request types for a parameterless endpoint", () => { + const code = generateApiTypesCode({ + session: { + mcp: { + list: { + rpcMethod: "session.mcp.list", + params: null, + }, + }, + }, + }); + expect(code).not.toContain("SessionMcpListParams"); + expect(code).not.toContain("McpListRequest"); + }); + it.each([ { sentinel: { type: "null" }, diff --git a/python/copilot/generated/rpc.py b/python/copilot/generated/rpc.py index e30e815dea..499f4baf00 100644 --- a/python/copilot/generated/rpc.py +++ b/python/copilot/generated/rpc.py @@ -305,6 +305,7 @@ class AuthInfoType(Enum): Authentication type. """ + ACCOUNT = "account" API_KEY = "api-key" COPILOT_API_TOKEN = "copilot-api-token" ENV = "env" @@ -347,6 +348,9 @@ def to_dict(self) -> dict: result["token"] = from_union([from_str, from_none], self.token) return result +class AccountAuthInfoType(Enum): + ACCOUNT = "account" + # Experimental: this type is part of an experimental API and may change or be removed. @dataclass class AccountGetCurrentAuthResult: @@ -462,6 +466,8 @@ class AccountKind(Enum): The provider kind of this account. + Provider kind that owns this account choice. + The neutral provider kind. A provider a consumer may interactively sign in with. @@ -843,12 +849,13 @@ class AuthLoginStepKind(Enum): # Experimental: this type is part of an experimental API and may change or be removed. class AuthLoginResultStatus(Enum): - """Terminal disposition of the login. + """Current disposition of the login, including pending user decisions. - Terminal disposition of a login persistence attempt. + Disposition of a login attempt, including pending user decisions. """ COMPLETED = "completed" DECLINED = "declined" + NEEDS_ACCOUNT_SELECTION = "needs-account-selection" NEEDS_PLAINTEXT_CONSENT = "needs-plaintext-consent" # Experimental: this type is part of an experimental API and may change or be removed. @@ -6586,6 +6593,34 @@ def to_dict(self) -> dict: result["changed"] = from_bool(self.changed) return result +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class MCPConfiguredServerState: + """Observed state from an already materialized matching server. Omitted when no live graph + has this configured server; it never determines configuration enablement. + + Observational state for a matching already materialized MCP server. + """ + status: McpServerStatus + """Observed connection status. This is not a configuration or readiness guarantee.""" + + error: str | None = None + """Observed connection error, when the materialized server failed.""" + + @staticmethod + def from_dict(obj: Any) -> 'MCPConfiguredServerState': + assert isinstance(obj, dict) + status = McpServerStatus(obj.get("status")) + error = from_union([from_str, from_none], obj.get("error")) + return MCPConfiguredServerState(status, error) + + def to_dict(self) -> dict: + result: dict = {} + result["status"] = to_enum(McpServerStatus, self.status) + if self.error is not None: + result["error"] = from_union([from_str, from_none], self.error) + return result + # Experimental: this type is part of an experimental API and may change or be removed. @dataclass class MCPDisableRequest: @@ -13757,6 +13792,7 @@ def to_dict(self) -> dict: return result class SettableAuthInfoType(Enum): + ACCOUNT = "account" API_KEY = "api-key" COPILOT_API_TOKEN = "copilot-api-token" ENV = "env" @@ -20309,35 +20345,65 @@ def to_dict(self) -> dict: # Experimental: this type is part of an experimental API and may change or be removed. @dataclass -class AuthLoginResultDto: - """The terminal login result. - - Terminal result of an interactive login flow. +class AccountAuthInfo: + """An interactive account whose model provider owns its credentials. It carries no GitHub + credential. """ - status: AuthLoginResultStatus - """Terminal disposition of the login.""" + host: str + """Host coordinate owned by the account's model provider.""" - host: str | None = None - """Host that was signed in, when completed.""" + login: str + """Login identifying the provider-owned account.""" - login: str | None = None - """Login that was signed in, when completed.""" + type: ClassVar[str] = "account" + """Provider-owned account authentication.""" @staticmethod - def from_dict(obj: Any) -> 'AuthLoginResultDto': + def from_dict(obj: Any) -> 'AccountAuthInfo': assert isinstance(obj, dict) - status = AuthLoginResultStatus(obj.get("status")) - host = from_union([from_str, from_none], obj.get("host")) - login = from_union([from_str, from_none], obj.get("login")) - return AuthLoginResultDto(status, host, login) + host = from_str(obj.get("host")) + login = from_str(obj.get("login")) + return AccountAuthInfo(host, login) def to_dict(self) -> dict: result: dict = {} - result["status"] = to_enum(AuthLoginResultStatus, self.status) - if self.host is not None: - result["host"] = from_union([from_str, from_none], self.host) - if self.login is not None: - result["login"] = from_union([from_str, from_none], self.login) + result["host"] = from_str(self.host) + result["login"] = from_str(self.login) + result["type"] = self.type + return result + +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class AuthLoginAccount: + """A credential-free account choice after sign-in.""" + + host: str + """Host coordinate owned by the selected account's provider.""" + + kind: AccountKind + """Provider kind that owns this account choice.""" + + login: str + """Human-readable login for the account choice.""" + + selection_id: str + """Opaque identifier supplied to the next login step to select this account.""" + + @staticmethod + def from_dict(obj: Any) -> 'AuthLoginAccount': + assert isinstance(obj, dict) + host = from_str(obj.get("host")) + kind = AccountKind(obj.get("kind")) + login = from_str(obj.get("login")) + selection_id = from_str(obj.get("selectionId")) + return AuthLoginAccount(host, kind, login, selection_id) + + def to_dict(self) -> dict: + result: dict = {} + result["host"] = from_str(self.host) + result["kind"] = to_enum(AccountKind, self.kind) + result["login"] = from_str(self.login) + result["selectionId"] = from_str(self.selection_id) return result # Experimental: this type is part of an experimental API and may change or be removed. @@ -26609,6 +26675,63 @@ def to_dict(self) -> dict: result["description"] = from_union([from_str, from_none], self.description) return result +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class MCPConfiguredServer: + """Effective MCP configuration entry. Configuration enablement is distinct from the optional + live observation. + """ + enabled: bool + """Whether this configured server is enabled after session configuration and policy + filtering. + """ + name: str + """Server name (config key)""" + + display_name: str | None = None + """Human-readable display name supplied by configuration.""" + + live: MCPConfiguredServerState | None = None + """Observed state from an already materialized matching server. Omitted when no live graph + has this configured server; it never determines configuration enablement. + """ + source: McpServerSource | None = None + """Configuration provenance: user, workspace, plugin, builtin, or managed.""" + + source_plugin: str | None = None + """Plugin name that provided this server, when source is plugin.""" + + source_plugin_version: str | None = None + """Plugin version that provided this server, when source is plugin.""" + + @staticmethod + def from_dict(obj: Any) -> 'MCPConfiguredServer': + assert isinstance(obj, dict) + enabled = from_bool(obj.get("enabled")) + name = from_str(obj.get("name")) + display_name = from_union([from_str, from_none], obj.get("displayName")) + live = from_union([MCPConfiguredServerState.from_dict, from_none], obj.get("live")) + source = from_union([McpServerSource, from_none], obj.get("source")) + source_plugin = from_union([from_str, from_none], obj.get("sourcePlugin")) + source_plugin_version = from_union([from_str, from_none], obj.get("sourcePluginVersion")) + return MCPConfiguredServer(enabled, name, display_name, live, source, source_plugin, source_plugin_version) + + def to_dict(self) -> dict: + result: dict = {} + result["enabled"] = from_bool(self.enabled) + result["name"] = from_str(self.name) + if self.display_name is not None: + result["displayName"] = from_union([from_str, from_none], self.display_name) + if self.live is not None: + result["live"] = from_union([lambda x: to_class(MCPConfiguredServerState, x), from_none], self.live) + if self.source is not None: + result["source"] = from_union([lambda x: to_enum(McpServerSource, x), from_none], self.source) + if self.source_plugin is not None: + result["sourcePlugin"] = from_union([from_str, from_none], self.source_plugin) + if self.source_plugin_version is not None: + result["sourcePluginVersion"] = from_union([from_str, from_none], self.source_plugin_version) + return result + # Experimental: this type is part of an experimental API and may change or be removed. @dataclass class MCPServer: @@ -32358,52 +32481,41 @@ def to_dict(self) -> dict: # Experimental: this type is part of an experimental API and may change or be removed. @dataclass -class AuthLoginStep: - """The first step of the flow. +class AuthLoginResultDto: + """Login result. When status is needs-plaintext-consent or needs-account-selection, advance + with the user's decision to continue. - One step in an interactive login flow. The consumer acts on the step and calls advance to - proceed. Browser-open is encoded as two distinct steps by design: `open-url` is - CONSUMER-driven (the provider surfaces the authorize URL and the consumer opens it — - github.com/GHEC web), while `needs-interaction` is PROVIDER-driven (the provider opens - the browser or broker UI itself and does not surface a URL — Entra). + Result of an interactive login flow. Pending consent or account selection is not terminal. """ - kind: AuthLoginStepKind - """Login flow step variant discriminator.""" - - url: str | None = None - """Authorize URL the consumer should open in a browser (consumer-driven browser-open).""" - - prompt: str | None = None - """Prompt for the value the provider needs; the consumer supplies it as advance input (e.g. - a GitHub Enterprise Cloud host, *.ghe.com). + status: AuthLoginResultStatus + """Current disposition of the login, including pending user decisions.""" + host: str | None = None + """Host that was signed in, when completed.""" + login: str | None = None + """Login that was signed in, when completed.""" + accounts: list[AuthLoginAccount] | None = None + """Available accounts when sign-in is awaiting account selection, ordered with Microsoft 365 + first. """ - result: AuthLoginResultDto | None = None - """The terminal login result.""" - - message: str | None = None - """Human-readable failure message.""" @staticmethod - def from_dict(obj: Any) -> 'AuthLoginStep': + def from_dict(obj: Any) -> 'AuthLoginResultDto': assert isinstance(obj, dict) - kind = AuthLoginStepKind(obj.get("kind")) - url = from_union([from_str, from_none], obj.get("url")) - prompt = from_union([from_str, from_none], obj.get("prompt")) - result = from_union([AuthLoginResultDto.from_dict, from_none], obj.get("result")) - message = from_union([from_str, from_none], obj.get("message")) - return AuthLoginStep(kind, url, prompt, result, message) + status = AuthLoginResultStatus(obj.get("status")) + accounts = from_union([lambda x: from_list(AuthLoginAccount.from_dict, x), from_none], obj.get("accounts")) + host = from_union([from_str, from_none], obj.get("host")) + login = from_union([from_str, from_none], obj.get("login")) + return AuthLoginResultDto(status, host, login, accounts) def to_dict(self) -> dict: result: dict = {} - result["kind"] = to_enum(AuthLoginStepKind, self.kind) - if self.url is not None: - result["url"] = from_union([from_str, from_none], self.url) - if self.prompt is not None: - result["prompt"] = from_union([from_str, from_none], self.prompt) - if self.result is not None: - result["result"] = from_union([lambda x: to_class(AuthLoginResultDto, x), from_none], self.result) - if self.message is not None: - result["message"] = from_union([from_str, from_none], self.message) + result["status"] = to_enum(AuthLoginResultStatus, self.status) + if self.accounts is not None: + result["accounts"] = from_union([lambda x: from_list(lambda x: to_class(AuthLoginAccount, x), x), from_none], self.accounts) + if self.host is not None: + result["host"] = from_union([from_str, from_none], self.host) + if self.login is not None: + result["login"] = from_union([from_str, from_none], self.login) return result # Experimental: this type is part of an experimental API and may change or be removed. @@ -35449,6 +35561,26 @@ def to_dict(self) -> dict: result["plugins"] = from_list(lambda x: to_class(MarketplacePluginInfo, x), self.plugins) return result +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class MCPConfiguredServerList: + """Effective MCP configuration with optional live observations from matching already + materialized servers. + """ + servers: list[MCPConfiguredServer] + """Effective configured MCP servers.""" + + @staticmethod + def from_dict(obj: Any) -> 'MCPConfiguredServerList': + assert isinstance(obj, dict) + servers = from_list(MCPConfiguredServer.from_dict, obj.get("servers")) + return MCPConfiguredServerList(servers) + + def to_dict(self) -> dict: + result: dict = {} + result["servers"] = from_list(lambda x: to_class(MCPConfiguredServer, x), self.servers) + return result + # Experimental: this type is part of an experimental API and may change or be removed. @dataclass class MCPServerList: @@ -38883,26 +39015,53 @@ def to_dict(self) -> dict: # Experimental: this type is part of an experimental API and may change or be removed. @dataclass -class AuthLoginBegun: - """A started login flow: its opaque id and first step.""" +class AuthLoginStep: + """The first step of the flow. - flow_id: str - """Opaque flow id used to advance or cancel this login.""" + One step in an interactive login flow. The consumer acts on the step and calls advance to + proceed. Browser-open is encoded as two distinct steps by design: `open-url` is + CONSUMER-driven (the provider surfaces the authorize URL and the consumer opens it — + github.com/GHEC web), while `needs-interaction` is PROVIDER-driven (the provider opens + the browser or broker UI itself and does not surface a URL — Entra). + """ + kind: AuthLoginStepKind + """Login flow step variant discriminator.""" - step: AuthLoginStep - """The first step of the flow.""" + url: str | None = None + """Authorize URL the consumer should open in a browser (consumer-driven browser-open).""" + + prompt: str | None = None + """Prompt for the value the provider needs; the consumer supplies it as advance input (e.g. + a GitHub Enterprise Cloud host, *.ghe.com). + """ + result: AuthLoginResultDto | None = None + """Login result. When status is needs-plaintext-consent or needs-account-selection, advance + with the user's decision to continue. + """ + message: str | None = None + """Human-readable failure message.""" @staticmethod - def from_dict(obj: Any) -> 'AuthLoginBegun': + def from_dict(obj: Any) -> 'AuthLoginStep': assert isinstance(obj, dict) - flow_id = from_str(obj.get("flowId")) - step = AuthLoginStep.from_dict(obj.get("step")) - return AuthLoginBegun(flow_id, step) + kind = AuthLoginStepKind(obj.get("kind")) + url = from_union([from_str, from_none], obj.get("url")) + prompt = from_union([from_str, from_none], obj.get("prompt")) + result = from_union([AuthLoginResultDto.from_dict, from_none], obj.get("result")) + message = from_union([from_str, from_none], obj.get("message")) + return AuthLoginStep(kind, url, prompt, result, message) def to_dict(self) -> dict: result: dict = {} - result["flowId"] = from_str(self.flow_id) - result["step"] = to_class(AuthLoginStep, self.step) + result["kind"] = to_enum(AuthLoginStepKind, self.kind) + if self.url is not None: + result["url"] = from_union([from_str, from_none], self.url) + if self.prompt is not None: + result["prompt"] = from_union([from_str, from_none], self.prompt) + if self.result is not None: + result["result"] = from_union([lambda x: to_class(AuthLoginResultDto, x), from_none], self.result) + if self.message is not None: + result["message"] = from_union([from_str, from_none], self.message) return result # Experimental: this type is part of an experimental API and may change or be removed. @@ -41440,6 +41599,30 @@ def to_dict(self) -> dict: result["status"] = to_class(ConnectorStatus, self.status) return result +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class AuthLoginBegun: + """A started login flow: its opaque id and first step.""" + + flow_id: str + """Opaque flow id used to advance or cancel this login.""" + + step: AuthLoginStep + """The first step of the flow.""" + + @staticmethod + def from_dict(obj: Any) -> 'AuthLoginBegun': + assert isinstance(obj, dict) + flow_id = from_str(obj.get("flowId")) + step = AuthLoginStep.from_dict(obj.get("step")) + return AuthLoginBegun(flow_id, step) + + def to_dict(self) -> dict: + result: dict = {} + result["flowId"] = from_str(self.flow_id) + result["step"] = to_class(AuthLoginStep, self.step) + return result + # Experimental: this type is part of an experimental API and may change or be removed. @dataclass class WorkflowRunSummary: @@ -47427,6 +47610,7 @@ class RPC: abort_result: AbortResult accepted_enqueue_command_result: AcceptedEnqueueCommandResult account_all_users: AccountAllUsers + account_auth_info: AccountAuthInfo account_get_all_users_result: list[AccountAllUsers] account_get_current_auth_result: AccountGetCurrentAuthResult account_get_quota_request: AccountGetQuotaRequest @@ -47479,6 +47663,7 @@ class RPC: auth_identity_metadata: AuthIdentityMetadata auth_info: AuthInfo auth_info_type: AuthInfoType + auth_login_account: AuthLoginAccount auth_login_advance_request: AuthLoginAdvanceRequest auth_login_begin_request: AuthLoginBeginRequest auth_login_begun: AuthLoginBegun @@ -47925,6 +48110,9 @@ class RPC: mcp_config_list: MCPConfigList mcp_config_remove_request: MCPConfigRemoveRequest mcp_config_update_request: MCPConfigUpdateRequest + mcp_configured_server: MCPConfiguredServer + mcp_configured_server_list: MCPConfiguredServerList + mcp_configured_server_state: MCPConfiguredServerState mcp_configure_git_hub_request: MCPConfigureGitHubRequest mcp_configure_git_hub_result: MCPConfigureGitHubResult mcp_diagnostic_details: MCPDiagnosticDetails @@ -48958,6 +49146,7 @@ def from_dict(obj: Any) -> 'RPC': abort_result = AbortResult.from_dict(obj.get("AbortResult")) accepted_enqueue_command_result = AcceptedEnqueueCommandResult.from_dict(obj.get("AcceptedEnqueueCommandResult")) account_all_users = AccountAllUsers.from_dict(obj.get("AccountAllUsers")) + account_auth_info = AccountAuthInfo.from_dict(obj.get("AccountAuthInfo")) account_get_all_users_result = from_list(AccountAllUsers.from_dict, obj.get("AccountGetAllUsersResult")) account_get_current_auth_result = AccountGetCurrentAuthResult.from_dict(obj.get("AccountGetCurrentAuthResult")) account_get_quota_request = AccountGetQuotaRequest.from_dict(obj.get("AccountGetQuotaRequest")) @@ -49010,6 +49199,7 @@ def from_dict(obj: Any) -> 'RPC': auth_identity_metadata = AuthIdentityMetadata.from_dict(obj.get("AuthIdentityMetadata")) auth_info = _load_AuthInfo(obj.get("AuthInfo")) auth_info_type = AuthInfoType(obj.get("AuthInfoType")) + auth_login_account = AuthLoginAccount.from_dict(obj.get("AuthLoginAccount")) auth_login_advance_request = AuthLoginAdvanceRequest.from_dict(obj.get("AuthLoginAdvanceRequest")) auth_login_begin_request = AuthLoginBeginRequest.from_dict(obj.get("AuthLoginBeginRequest")) auth_login_begun = AuthLoginBegun.from_dict(obj.get("AuthLoginBegun")) @@ -49456,6 +49646,9 @@ def from_dict(obj: Any) -> 'RPC': mcp_config_list = MCPConfigList.from_dict(obj.get("McpConfigList")) mcp_config_remove_request = MCPConfigRemoveRequest.from_dict(obj.get("McpConfigRemoveRequest")) mcp_config_update_request = MCPConfigUpdateRequest.from_dict(obj.get("McpConfigUpdateRequest")) + mcp_configured_server = MCPConfiguredServer.from_dict(obj.get("McpConfiguredServer")) + mcp_configured_server_list = MCPConfiguredServerList.from_dict(obj.get("McpConfiguredServerList")) + mcp_configured_server_state = MCPConfiguredServerState.from_dict(obj.get("McpConfiguredServerState")) mcp_configure_git_hub_request = MCPConfigureGitHubRequest.from_dict(obj.get("McpConfigureGitHubRequest")) mcp_configure_git_hub_result = MCPConfigureGitHubResult.from_dict(obj.get("McpConfigureGitHubResult")) mcp_diagnostic_details = MCPDiagnosticDetails.from_dict(obj.get("McpDiagnosticDetails")) @@ -50481,7 +50674,7 @@ def from_dict(obj: Any) -> 'RPC': subagent_settings = from_union([SubagentSettings.from_dict, from_none], obj.get("SubagentSettings")) task_progress = from_union([TaskProgress.from_dict, from_none], obj.get("TaskProgress")) workspace_summary = from_union([WorkspaceSummary.from_dict, from_none], obj.get("WorkspaceSummary")) - return RPC(abort_request, abort_result, accepted_enqueue_command_result, account_all_users, account_get_all_users_result, account_get_current_auth_result, account_get_quota_request, account_get_quota_result, account_kind, account_login_request, account_login_result, account_logout_request, account_logout_result, account_quota_snapshot, accounts_enumerate_request, accounts_get_request, accounts_set_request, account_status, adaptive_thinking_support, agent_discovery_path, agent_discovery_path_list, agent_discovery_path_scope, agent_get_current_result, agent_info, agent_info_source, agent_list, agent_list_request, agent_registry_live_target_entry, agent_registry_live_target_entry_attention_kind, agent_registry_live_target_entry_kind, agent_registry_live_target_entry_last_terminal_event, agent_registry_live_target_entry_status, agent_registry_log_capture, agent_registry_log_capture_open_error_reason, agent_registry_spawn_error, agent_registry_spawn_permission_mode, agent_registry_spawn_registry_timeout, agent_registry_spawn_request, agent_registry_spawn_result, agent_registry_spawn_spawned, agent_registry_spawn_validation_error, agent_registry_spawn_validation_error_field, agent_registry_spawn_validation_error_reason, agent_reload_result, agents_discover_request, agent_select_request, agent_select_result, agent_set_prompt_request, agents_get_discovery_paths_request, api_key_auth_info, auth_enumerate_query, auth_enumerate_value, auth_identity, auth_identity_metadata, auth_info, auth_info_type, auth_login_advance_request, auth_login_begin_request, auth_login_begun, auth_login_cancel_request, auth_login_result_dto, auth_login_result_status, auth_login_step, auth_read_query, auth_read_value, auth_status_dto, auth_validation_error, auth_validation_errors, auth_write, auth_write_result, autopilot_objective_credit_limit, autopilot_objective_get_state_result, autopilot_objective_state, autopilot_objective_status, built_in_model_catalog, built_in_model_catalog_entry, builtin_tool_descriptor, builtin_tool_format, builtin_tool_format_type, builtin_tool_input_schema, builtin_tool_input_schema_type, builtin_tool_safe_for_telemetry, builtin_tool_safe_telemetry_fields, cancel_user_requested_shell_command_result, canvas_action, canvas_action_invoke_request, canvas_action_invoke_result, canvas_close_request, canvas_host_context, canvas_host_context_capabilities, canvas_json_schema, canvas_list, canvas_list_open_result, canvas_open_request, canvas_provider_close_request, canvas_provider_invoke_action_request, canvas_provider_open_request, canvas_provider_open_result, canvas_provider_register_request, canvas_provider_unregister_request, canvas_session_context, capi_session_options, card_digest, card_digest_algorithm, card_digest_value, catalog_agent_plugin_candidate, catalog_agent_plugin_candidate_kind, catalog_agent_plugin_candidate_provenance, catalog_agent_plugin_compatibility_tag, catalog_agent_plugin_media_type, catalog_ai_skill_candidate, catalog_ai_skill_candidate_kind, catalog_ai_skill_candidate_provenance, catalog_ai_skill_installability, catalog_ai_skill_media_type, catalog_authentication_required_error, catalog_authentication_required_reason, catalog_candidate, catalog_candidate_kind, catalog_candidate_source, catalog_candidate_source_embedded, catalog_candidate_source_url, catalog_capability, catalog_capability_id, catalog_client_contract, catalog_contract_violation_error, catalog_contract_violation_reason, catalog_handle_rejected_error, catalog_handle_rejection_reason, catalog_handle_type, catalog_invalid_request_error, catalog_invalid_request_field, catalog_malformed_card_error, catalog_malformed_card_reason, catalog_mcp_server_candidate, catalog_mcp_server_candidate_kind, catalog_mcp_server_candidate_provenance, catalog_mcp_server_installability, catalog_media_type, catalog_negotiated_contract, catalog_negotiation_refused_error, catalog_negotiation_refused_reason, catalog_network_failure_error, catalog_network_failure_reason, catalog_not_installable_error, catalog_not_installable_reason, catalog_plugin_repository_source, catalog_policy_rejected_error, catalog_resource_identity, catalog_resource_version, catalog_search_page, catalog_search_pagination, catalog_search_request, catalog_search_result, catalog_search_succeeded, catalog_search_total_count_relation, catalog_selection_cancelled, catalog_selection_decision, catalog_selection_declined, catalog_selection_foreign, catalog_selection_invalid, catalog_selection_replayed, catalog_selection_request, catalog_selection_result, catalog_selection_selected, catalog_selection_stale, catalog_selection_timed_out, catalog_selection_wrong_kind, catalog_trust_eligibility, catalog_trust_provenance, catalog_trust_snapshot, catalog_trust_snapshot_absent, catalog_trust_snapshot_absent_status, catalog_trust_snapshot_current, catalog_trust_snapshot_current_status, catalog_trust_snapshot_downgraded, catalog_trust_snapshot_downgraded_status, catalog_trust_snapshot_malformed, catalog_trust_snapshot_malformed_status, catalog_trust_snapshot_revoked, catalog_trust_snapshot_revoked_status, catalog_trust_snapshot_schema_version, catalog_trust_snapshot_stale, catalog_trust_snapshot_stale_status, catalog_trust_snapshot_unsupported, catalog_trust_snapshot_unsupported_status, catalog_trust_source, catalog_trust_tier, catalog_unavailable_error, catalog_unavailable_reason, catalog_unavailable_transport_error, catalog_unavailable_transport_reason, catalog_unsafe_retrieval_error, catalog_unsafe_retrieval_reason, catalog_unsupported_kind_error, client_metadata, client_task_cancel_reason, client_task_cancel_request, client_task_cancel_result, command_list, commands_finalize_invocation_effect_request, commands_finalize_invocation_effect_result, commands_handle_pending_command_request, commands_handle_pending_command_result, commands_invocation_effect_outcome, commands_invocation_origin, commands_invoke_request, commands_list_request, commands_respond_to_queued_command_request, commands_respond_to_queued_command_result, completions_get_trigger_characters_result, completions_request_request, completions_request_result, configure_session_extensions_params, connect_client_info, connected_remote_session_metadata, connected_remote_session_metadata_kind, connected_remote_session_metadata_repository, connector_account_request, connector_authorization_requirement, connector_authorization_scope, connector_availability, connector_capabilities, connector_catalog_entry, connector_catalog_result, connector_catalog_status, connector_connect_request, connector_connect_result, connector_continue_request, connector_disconnect_result, connector_mcp_status, connector_reconcile_request, connector_runtime_status, connector_session_account, connector_status, connect_remote_session_params, connect_request, connect_result, content_exclusion_check_paths_request, content_exclusion_check_paths_result, content_exclusion_path_check, content_filter_mode, context_heaviest_message, copilot_api_token_auth_info, copilot_user_response, copilot_user_response_endpoints, copilot_user_response_quota_snapshots, copilot_user_response_quota_snapshots_chat, copilot_user_response_quota_snapshots_completions, copilot_user_response_quota_snapshots_premium_interactions, current_model, current_tool_metadata, customization_reload_outcome, customization_reload_status, customization_reload_subsystem, customizations_reload_result, debug_collect_logs_collected_entry, debug_collect_logs_destination, debug_collect_logs_entry, debug_collect_logs_entry_kind, debug_collect_logs_include, debug_collect_logs_redaction, debug_collect_logs_request, debug_collect_logs_result, debug_collect_logs_result_kind, debug_collect_logs_skipped_entry, debug_collect_logs_source, diagnostic_cursor_status, diagnostic_entry, diagnostic_log_level, diagnostics_configuration, diagnostics_configure_request, diagnostic_severity, diagnostic_source, diagnostic_sources_configuration, diagnostics_read_request, diagnostics_read_result, discovered_canvas, discovered_extension, discovered_extension_mode, discovered_extension_plugin, discovered_extensions, discovered_extensions_disable_request, discovered_extensions_enable_request, discovered_extension_source, discovered_hook, discovered_mcp_server, discovered_mcp_server_type, discovered_model, discovered_model_list, enqueue_command_params, enqueue_command_result, env_auth_info, environment_capabilities, environment_kind, environments_delete_request, environments_delete_result, environments_get_request, environments_get_result, environments_list_request, environments_list_result, event_log_read_request, event_log_release_interest_result, event_log_tail_result, event_log_types, events_agent_scope, events_cursor_status, events_read_direction, events_read_result, execute_command_params, execute_command_result, extension, extension_context_push_input, extension_launch_profile, extension_launch_provider_resolve_request, extension_launch_provider_resolve_result, extension_list, extensions_disable_request, extensions_enable_request, extension_source, extension_status, external_tool_result, external_tool_text_result_for_llm, external_tool_text_result_for_llm_binary_results_for_llm, external_tool_text_result_for_llm_binary_results_for_llm_type, external_tool_text_result_for_llm_content, external_tool_text_result_for_llm_content_audio, external_tool_text_result_for_llm_content_image, external_tool_text_result_for_llm_content_resource, external_tool_text_result_for_llm_content_resource_details, external_tool_text_result_for_llm_content_resource_link, external_tool_text_result_for_llm_content_resource_link_icon, external_tool_text_result_for_llm_content_resource_link_icon_theme, external_tool_text_result_for_llm_content_shell_exit, external_tool_text_result_for_llm_content_terminal, external_tool_text_result_for_llm_content_text, filter_mapping, fleet_start_request, fleet_start_result, folder_trust_add_params, folder_trust_check_params, folder_trust_check_result, gh_cli_auth_info, git_hub_environment, git_hub_telemetry_client_info, git_hub_telemetry_event, git_hub_telemetry_notification, git_hub_token_acquire_reason, git_hub_token_acquire_request, git_hub_token_acquire_result, handle_pending_tool_call_request, handle_pending_tool_call_result, history_abort_manual_compaction_result, history_cancel_background_compaction_result, history_clear_context_request, history_clear_context_result, history_compact_context_window, history_compact_request, history_compact_result, history_file_restore_skip_reason, history_list_rewind_points_result, history_preview_rewind_request, history_preview_rewind_result, history_rewind_change_type, history_rewind_file_preview, history_rewind_mode, history_rewind_outcome, history_rewind_point, history_rewind_request, history_rewind_result, history_rewind_unavailable_reason, history_skipped_file_restore, history_summarize_for_handoff_result, history_truncate_request, history_truncate_result, hmac_auth_info, hook_invoke_request, hook_invoke_response, hook_origin, hooks_discover_request, hooks_discover_result, hook_type, host_configuration, host_dispose_request, host_empty_result, host_environment_credentials, host_exited_notification, host_exit_reason, host_git_hub_environment_options, host_local_server_configuration, host_local_server_options, host_publish_session_request, host_publish_session_result, host_ready_request, host_register_session_request, host_session_create_callback, host_session_create_request, host_session_create_result, host_session_released_notification, host_session_release_request, host_start_request, host_start_result, installation_catalogue_identity, installation_confirmation_request, installation_confirmation_response, installation_decision, installation_review, installed_plugin, installed_plugin_info, installed_plugin_source, installed_plugin_source_git_hub, installed_plugin_source_local, installed_plugin_source_url, instruction_discovery_path, instruction_discovery_path_kind, instruction_discovery_path_list, instruction_discovery_path_location, instructions_discover_request, instructions_get_discovery_paths_request, instructions_get_sources_result, instruction_source, instruction_source_location, instruction_source_type, interrupt_main_turn_request, interrupt_main_turn_result, json_schema_response_format, llm_inference_headers, llm_inference_http_request_chunk_request, llm_inference_http_request_chunk_result, llm_inference_http_request_start_request, llm_inference_http_request_start_result, llm_inference_http_request_start_transport, llm_inference_http_response_chunk_error, llm_inference_http_response_chunk_request, llm_inference_http_response_chunk_result, llm_inference_http_response_start_request, llm_inference_http_response_start_result, llm_inference_set_provider_result, local_session_metadata_value, login_provider_kind, log_request, log_result, lsp_initialize_request, managed_mcp_server_config, managed_setting_meta, managed_settings_channel, managed_settings_compose_layer, managed_settings_compose_request, managed_settings_compose_result, managed_settings_diagnostic, managed_settings_diagnostic_severity, managed_settings_layer, managed_settings_meta, managed_settings_read_result, managed_settings_resolved_data, managed_settings_resolve_request, managed_settings_resolve_result, managed_settings_schema_result, managed_settings_validate_request, managed_settings_validate_result, managed_settings_values, marketplace_add_result, marketplace_browse_result, marketplace_info, marketplace_list_result, marketplace_plugin_info, marketplace_refresh_entry, marketplace_refresh_result, marketplace_remove_result, mcp_allowed_server, mcp_apply_install_request, mcp_apply_uninstall_request, mcp_apps_call_tool_request, mcp_apps_diagnose_capability, mcp_apps_diagnose_request, mcp_apps_diagnose_result, mcp_apps_diagnose_server, mcp_apps_host_context, mcp_apps_host_context_details, mcp_apps_host_context_details_available_display_mode, mcp_apps_host_context_details_display_mode, mcp_apps_host_context_details_platform, mcp_apps_host_context_details_theme, mcp_apps_list_tools_request, mcp_apps_list_tools_result, mcp_apps_read_resource_request, mcp_apps_read_resource_result, mcp_apps_resource_content, mcp_apps_set_host_context_details, mcp_apps_set_host_context_details_available_display_mode, mcp_apps_set_host_context_details_display_mode, mcp_apps_set_host_context_details_platform, mcp_apps_set_host_context_details_theme, mcp_apps_set_host_context_request, mcp_cancel_sampling_execution_params, mcp_cancel_sampling_execution_result, mcp_config_add_request, mcp_config_disable_request, mcp_config_enable_request, mcp_config_list, mcp_config_remove_request, mcp_config_update_request, mcp_configure_git_hub_request, mcp_configure_git_hub_result, mcp_diagnostic_details, mcp_diagnostic_direction, mcp_diagnostic_kind, mcp_diagnostic_source_configuration, mcp_disable_request, mcp_discover_request, mcp_discover_result, mcp_elicitation_form_mode, mcp_enable_request, mcp_execute_sampling_params, mcp_execute_sampling_request, mcp_execute_sampling_result, mcp_failed_server, mcp_filtered_server, mcp_headers_handle_pending_headers_refresh_request, mcp_headers_handle_pending_headers_refresh_request_request, mcp_headers_handle_pending_headers_refresh_request_result, mcp_host_state, mcp_installation_failure_reason, mcp_installation_input, mcp_installation_management_outcome, mcp_installation_management_result, mcp_installation_operation_request, mcp_installation_operation_status, mcp_installation_outcome, mcp_installation_remote_configuration, mcp_installation_result, mcp_installation_review, mcp_installation_secret, mcp_installation_secret_storage, mcp_installations_request, mcp_installation_state, mcp_installation_summary, mcp_install_plan, mcp_is_server_running_request, mcp_is_server_running_result, mcp_list_tools_request, mcp_list_tools_result, mcp_oauth_authentication_state_changed_request, mcp_oauth_cancel_login_request, mcp_oauth_cancel_login_result, mcp_oauth_complete_request, mcp_oauth_handle_pending_request, mcp_oauth_handle_pending_result, mcp_oauth_login_grant_type, mcp_oauth_login_request, mcp_oauth_login_result, mcp_oauth_pending_request_response, mcp_oauth_prepare_login_request, mcp_oauth_prepare_login_result, mcp_oauth_probe_needs_auth_reason, mcp_oauth_probe_request, mcp_oauth_probe_result, mcp_oauth_respond_request, mcp_oauth_respond_result, mcp_owned_oauth_login_status, mcp_plan_configuration_change, mcp_plan_configuration_operation, mcp_plan_enum_value_type, mcp_plan_install_planned, mcp_plan_install_request, mcp_plan_install_result, mcp_plan_install_source, mcp_plan_install_source_candidate, mcp_plan_install_source_candidate_kind, mcp_plan_install_source_card, mcp_plan_install_source_card_kind, mcp_plan_package_install_method, mcp_plan_package_transport, mcp_plan_policy_decision, mcp_plan_policy_result, mcp_plan_policy_source, mcp_plan_provenance, mcp_plan_remote_install_method, mcp_plan_remote_transport, mcp_plan_required_value, mcp_plan_required_value_enum, mcp_plan_required_value_enum_kind, mcp_plan_required_value_scalar, mcp_plan_required_value_scalar_kind, mcp_plan_resource_identity, mcp_plan_scalar_value_type, mcp_plan_scope, mcp_plan_secret_placeholder, mcp_plan_secret_reference, mcp_plan_target, mcp_plan_transport_choice, mcp_plan_transport_choice_package, mcp_plan_transport_choice_remote, mcp_plan_uninstall_request, mcp_plan_value_category, mcp_prepared_install, mcp_prepare_install_request, mcp_prompt, mcp_prompt_argument, mcp_prompt_icon, mcp_prompt_message, mcp_prompt_role, mcp_prompts_get_request, mcp_prompts_get_result, mcp_prompts_list_request, mcp_prompts_list_result, mcp_register_external_client_request, mcp_reload_config, mcp_reload_with_config_request, mcp_remove_git_hub_result, mcp_resource, mcp_resource_annotations, mcp_resource_content, mcp_resource_icon, mcp_resources_list_request, mcp_resources_list_result, mcp_resources_list_templates_request, mcp_resources_list_templates_result, mcp_resources_read_request, mcp_resources_read_result, mcp_resource_template, mcp_restart_server_request, mcp_safe_for_telemetry, mcp_safe_for_telemetry_fields, mcp_sampling_execution_action, mcp_sampling_execution_result, mcp_serializable_server_config, mcp_server, mcp_server_auth_config, mcp_server_auth_config_redirect_port, mcp_server_card_embedded, mcp_server_card_embedded_kind, mcp_server_card_media_type, mcp_server_card_reference, mcp_server_card_url, mcp_server_card_url_kind, mcp_server_config, mcp_server_config_defer_tools, mcp_server_config_http, mcp_server_config_http_oauth_grant_type, mcp_server_config_http_type, mcp_server_config_memory, mcp_server_config_memory_type, mcp_server_config_stdio, mcp_server_config_stdio_type, mcp_server_failure_info, mcp_server_list, mcp_server_needs_auth_info, mcp_server_ownership, mcp_set_env_value_mode_details, mcp_set_env_value_mode_params, mcp_set_env_value_mode_result, mcp_source_file, mcp_source_plugin, mcp_source_ref, mcp_start_server_request, mcp_start_servers_result, mcp_stop_server_request, mcp_task_metadata, mcp_tools, mcp_tool_ui, mcp_tool_ui_visibility, mcp_uninstall_plan, mcp_unregister_external_client_request, memory_configuration, metadata_context_attribution_result, metadata_context_heaviest_messages_request, metadata_context_heaviest_messages_result, metadata_context_info_request, metadata_context_info_result, metadata_is_processing_result, metadata_recompute_context_tokens_request, metadata_recompute_context_tokens_result, metadata_record_context_change_request, metadata_record_context_change_result, metadata_set_working_directory_request, metadata_set_working_directory_result, metadata_snapshot_current_mode, metadata_snapshot_remote_metadata, metadata_snapshot_remote_metadata_repository, metadata_snapshot_remote_metadata_task_type, metadata_update_client_metadata_request, model, model_apply_startup_overlay_request, model_artifact_details, model_billing, model_billing_promo, model_billing_token_prices, model_billing_token_prices_long_context, model_capabilities, model_capabilities_limits, model_capabilities_limits_vision, model_capabilities_override, model_capabilities_override_limits, model_capabilities_override_limits_vision, model_capabilities_override_supports, model_capabilities_supports, model_list, model_list_request, model_message, model_picker_category, model_picker_persistence_request, model_picker_price_category, model_picker_settings_context, model_policy, model_policy_state, model_provider_adapter_catalog, model_provider_adapter_descriptor, model_provider_adapter_operation_descriptor, model_provider_attribution, model_provider_automatic_discovery_mode, model_provider_automatic_discovery_policy, model_provider_configuration_disposition, model_provider_configuration_plan, model_provider_descriptor, model_provider_discover_request, model_provider_discover_result, model_provider_discovery_network_scope, model_provider_get_status_request, model_provider_instance, model_provider_instance_reference, model_provider_kind, model_provider_models_list_request, model_provider_operation_outcome, model_provider_operation_outcome_code, model_provider_prepare_configuration_request, model_provider_provenance, model_provider_provenance_source, model_provider_ref, model_provider_status, model_provider_warning, model_set_allowed_models_request, model_set_allowed_models_result, model_set_reasoning_effort_request, model_set_reasoning_effort_result, models_list_request, model_switch_auto_tier_request, model_switch_auto_tier_result, model_switch_auto_tier_status, model_switch_confirmation, model_switch_to_request, model_switch_to_result, model_warning_text, mode_set_request, mode_set_result, move_mcp_loading_to_background_result, named_provider_config, name_get_result, name_set_auto_request, name_set_auto_result, name_set_request, open_canvas_instance, options_update_additional_content_exclusion_policy, options_update_additional_content_exclusion_policy_rule, options_update_additional_content_exclusion_policy_rule_source, options_update_additional_content_exclusion_policy_scope, options_update_context_tier, options_update_env_value_mode, options_update_reasoning_summary, options_update_tool_filter_precedence, pending_permission_request, pending_permission_request_list, permission_decision, permission_decision_approved, permission_decision_approved_for_location, permission_decision_approved_for_session, permission_decision_approve_for_location, permission_decision_approve_for_location_approval, permission_decision_approve_for_location_approval_commands, permission_decision_approve_for_location_approval_custom_tool, permission_decision_approve_for_location_approval_extension_env_access, permission_decision_approve_for_location_approval_extension_management, permission_decision_approve_for_location_approval_extension_permission_access, permission_decision_approve_for_location_approval_mcp, permission_decision_approve_for_location_approval_mcp_sampling, permission_decision_approve_for_location_approval_memory, permission_decision_approve_for_location_approval_read, permission_decision_approve_for_location_approval_workflow, permission_decision_approve_for_location_approval_write, permission_decision_approve_for_session, permission_decision_approve_for_session_approval, permission_decision_approve_for_session_approval_commands, permission_decision_approve_for_session_approval_custom_tool, permission_decision_approve_for_session_approval_extension_env_access, permission_decision_approve_for_session_approval_extension_management, permission_decision_approve_for_session_approval_extension_permission_access, permission_decision_approve_for_session_approval_mcp, permission_decision_approve_for_session_approval_mcp_sampling, permission_decision_approve_for_session_approval_memory, permission_decision_approve_for_session_approval_read, permission_decision_approve_for_session_approval_workflow, permission_decision_approve_for_session_approval_write, permission_decision_approve_once, permission_decision_approve_permanently, permission_decision_approve_read_only_for_session, permission_decision_cancelled, permission_decision_context, permission_decision_denied_by_content_exclusion_policy, permission_decision_denied_by_permission_request_hook, permission_decision_denied_by_rules, permission_decision_denied_interactively_by_user, permission_decision_denied_no_approval_rule_and_could_not_request_from_user, permission_decision_outcome, permission_decision_reject, permission_decision_request, permission_decision_surface, permission_decision_user_not_available, permission_location_add_tool_approval_params, permission_location_apply_params, permission_location_apply_result, permission_location_resolve_params, permission_location_resolve_result, permission_location_type, permission_mode_source, permission_paths_add_params, permission_paths_allowed_check_params, permission_paths_allowed_check_result, permission_paths_config, permission_paths_list, permission_paths_update_primary_params, permission_paths_workspace_check_params, permission_paths_workspace_check_result, permission_prompt_shown_notification, permission_request_result, permission_response_capability, permission_rules_set, permissions_configure_additional_content_exclusion_policy, permissions_configure_additional_content_exclusion_policy_rule, permissions_configure_additional_content_exclusion_policy_rule_source, permissions_configure_additional_content_exclusion_policy_scope, permissions_configure_params, permissions_configure_result, permissions_folder_trust_add_trusted_result, permissions_get_mode_request, permissions_get_mode_result, permissions_locations_add_tool_approval_details, permissions_locations_add_tool_approval_details_commands, permissions_locations_add_tool_approval_details_custom_tool, permissions_locations_add_tool_approval_details_extension_env_access, permissions_locations_add_tool_approval_details_extension_management, permissions_locations_add_tool_approval_details_extension_permission_access, permissions_locations_add_tool_approval_details_mcp, permissions_locations_add_tool_approval_details_mcp_sampling, permissions_locations_add_tool_approval_details_memory, permissions_locations_add_tool_approval_details_read, permissions_locations_add_tool_approval_details_workflow, permissions_locations_add_tool_approval_details_write, permissions_locations_add_tool_approval_result, permissions_modify_rules_params, permissions_modify_rules_result, permissions_modify_rules_scope, permissions_notify_prompt_shown_result, permissions_paths_add_result, permissions_paths_list_request, permissions_paths_update_primary_result, permissions_pending_requests_request, permissions_reset_session_approvals_request, permissions_reset_session_approvals_result, permissions_set_approve_all_request, permissions_set_approve_all_result, permissions_set_approve_all_source, permissions_set_mode_request, permissions_set_mode_result, permissions_set_required_request, permissions_set_required_result, permissions_urls_set_unrestricted_mode_result, permission_urls_config, permission_urls_set_unrestricted_mode_params, ping_request, ping_result, plan_read_result, plan_read_sql_todos_result, plan_read_sql_todos_with_dependencies_result, plan_sql_todo_dependency, plan_sql_todos_row, plan_update_request, plugin, plugin_install_result, plugin_install_staging_mode, plugin_list, plugin_list_result, plugins_builtin_set_request, plugins_disable_request, plugins_enable_request, plugins_install_request, plugins_marketplaces_add_request, plugins_marketplaces_browse_request, plugins_marketplaces_refresh_request, plugins_marketplaces_remove_request, plugins_reload_request, plugins_uninstall_request, plugins_update_request, plugin_update_all_entry, plugin_update_all_result, plugin_update_result, protocol_append_mode, protocol_customize_mode, protocol_external_tool_defer, protocol_external_tool_definition, protocol_marker_section_override, protocol_replace_mode, protocol_section_override, protocol_static_section_action, protocol_static_section_override, protocol_system_message_append_config, protocol_system_message_config, protocol_system_message_customize_config, protocol_system_message_replace_config, provider_add_request, provider_add_result, provider_config, provider_config_azure, provider_config_model_provider, provider_config_transport, provider_config_type, provider_config_wire_api, provider_descriptor, provider_endpoint, provider_endpoint_transport, provider_endpoint_type, provider_endpoint_wire_api, provider_get_endpoint_request, provider_model_config, provider_session_token, provider_sync_request, provider_sync_result, provider_token_acquire_request, provider_token_acquire_result, provider_withdraw_request, provider_withdraw_result, push_attachment, push_attachment_blob, push_attachment_directory, push_attachment_file, push_attachment_file_line_range, push_attachment_git_hub_actions_job, push_attachment_git_hub_commit, push_attachment_git_hub_file, push_attachment_git_hub_file_diff, push_attachment_git_hub_file_diff_side, push_attachment_git_hub_reference, push_attachment_git_hub_reference_type, push_attachment_git_hub_release, push_attachment_git_hub_repository, push_attachment_git_hub_snippet, push_attachment_git_hub_tree_comparison, push_attachment_git_hub_tree_comparison_side, push_attachment_git_hub_url, push_attachment_selection, push_attachment_selection_details, push_attachment_selection_details_end, push_attachment_selection_details_start, push_git_hub_repo_ref, queue_append_steering_request, queue_begin_deferred_idle_drain_request, queue_begin_deferred_idle_drain_result, queue_consume_system_notifications_request, queued_command_handled, queued_command_not_handled, queued_command_result, queue_defer_session_idle_request, queue_duplicate_at_request, queue_duplicate_at_result, queue_enqueue_resume_pending_result, queue_finish_deferred_idle_drain_request, queue_finish_deferred_idle_drain_result, queue_has_pending_result, queue_insert_at_request, queue_insert_at_result, queue_insert_message, queue_move_item_request, queue_move_item_result, queue_pending_items, queue_pending_items_kind, queue_pending_items_result, queue_remove_at_request, queue_remove_at_result, queue_remove_most_recent_result, queue_send_now_request, queue_send_now_result, queue_set_drain_paused_request, queue_snapshot_result, queue_update_text_request, queue_update_text_result, queue_withdraw_message_request, queue_withdraw_message_result, register_event_interest_params, register_event_interest_result, release_event_interest_params, remote_control_config, remote_control_config_existing_mc_session, remote_control_status, remote_control_status_active, remote_control_status_connecting, remote_control_status_error, remote_control_status_off, remote_control_status_result, remote_control_stop_result, remote_control_transfer_result, remote_enable_request, remote_enable_result, remote_notify_steerable_changed_request, remote_notify_steerable_changed_result, remote_session_connection_result, remote_session_host_status, remote_session_metadata_repository, remote_session_metadata_task_type, remote_session_metadata_value, remote_session_mode, remote_session_repository, response_format, sandbox_config, sandbox_config_auth, sandbox_config_source, sandbox_config_user_policy, sandbox_config_user_policy_experimental, sandbox_config_user_policy_experimental_seatbelt, sandbox_config_user_policy_filesystem, sandbox_config_user_policy_network, sandbox_config_user_policy_network_proxy, sandbox_config_user_policy_seatbelt, sandbox_credentials_config, sandbox_disable_for_session_request, sandbox_disable_for_session_result, sandbox_enforcement_status, sandbox_grant_path_for_request_request, sandbox_grant_path_for_request_result, sandbox_host_capability, sandbox_host_capability_name, sandbox_host_support, sandbox_masked_env_var, sandbox_proxy_ca_create_result, sandbox_proxy_ca_request, sandbox_proxy_ca_state, sandbox_proxy_ca_status, sandbox_session_change, schedule_add_at_request, schedule_add_cron_request, schedule_add_request, schedule_add_result, schedule_add_self_paced_request, schedule_entry, schedule_has_self_paced_result, schedule_list, schedule_rearm_self_paced_request, schedule_stop_request, schedule_stop_result, secrets_add_filter_values_request, secrets_add_filter_values_result, send_agent_mode, send_attachments_to_message_params, send_message_item, send_messages_request, send_messages_result, send_mode, send_request, send_result, send_system_notification_request, server_agent_list, server_instruction_source_list, server_skill, server_skill_list, session_activity, session_agent_list_request, session_auth_login_request, session_auth_logout_user_request, session_auth_status, session_auth_switch_request, session_bulk_delete_result, session_cancel_all_background_agents_result, session_capability, session_commands_list_request, session_completion_item, session_context, session_context_host_type, session_enrich_metadata_result, session_fs_append_file_request, session_fs_error, session_fs_error_code, session_fs_exists_request, session_fs_exists_result, session_fs_mkdir_request, session_fs_readdir_request, session_fs_readdir_result, session_fs_readdir_with_types_entry, session_fs_readdir_with_types_entry_type, session_fs_readdir_with_types_request, session_fs_readdir_with_types_result, session_fs_read_file_bytes_request, session_fs_read_file_bytes_result, session_fs_read_file_request, session_fs_read_file_result, session_fs_rename_request, session_fs_rm_request, session_fs_set_provider_capabilities, session_fs_set_provider_conventions, session_fs_set_provider_request, session_fs_set_provider_result, session_fs_sqlite_exists_request, session_fs_sqlite_exists_result, session_fs_sqlite_query_request, session_fs_sqlite_query_result, session_fs_sqlite_query_type, session_fs_sqlite_transaction_error, session_fs_sqlite_transaction_error_class, session_fs_sqlite_transaction_request, session_fs_sqlite_transaction_result, session_fs_sqlite_transaction_statement, session_fs_stat_request, session_fs_stat_result, session_fs_write_file_bytes_request, session_fs_write_file_request, session_git_hub_auth_get_all_auth_available_result, session_git_hub_auth_logout_result, session_git_hub_auth_logout_user_result, session_history_compact_request, session_installed_plugin, session_installed_plugin_source, session_installed_plugin_source_git_hub, session_installed_plugin_source_local, session_installed_plugin_source_url, session_limit_prediction_baseline_data, session_limit_prediction_client_type, session_limit_prediction_details, session_limit_prediction_predict_request, session_limit_prediction_request, session_limit_prediction_result, session_limit_prediction_source, session_limit_prediction_tier, session_limit_prediction_tier_option, session_limit_prediction_unavailable_reason, session_list, session_list_entry, session_list_filter, session_load_deferred_repo_hooks_result, session_log_level, session_managed_permissions, session_managed_settings, session_mcp_apps_call_tool_result, session_mcp_oauth_cancel_login_request, session_mcp_oauth_cancel_login_result, session_mcp_oauth_prepare_login_request, session_mcp_oauth_prepare_login_result, session_metadata_snapshot, session_mode, session_model_list, session_model_list_request, session_model_price_category, session_open_options, session_open_options_additional_content_exclusion_policy, session_open_options_additional_content_exclusion_policy_rule, session_open_options_additional_content_exclusion_policy_rule_source, session_open_options_additional_content_exclusion_policy_scope, session_open_options_env_value_mode, session_open_options_reasoning_summary, session_open_params, session_open_result, session_plugins_disable_request, session_plugins_enable_request, session_plugins_install_request, session_plugins_marketplaces_refresh_request, session_plugins_reload_request, session_provider_get_endpoint_request, session_prune_result, sessions_bulk_delete_request, sessions_check_in_use_request, sessions_check_in_use_result, sessions_client_metadata_entry, sessions_close_request, sessions_close_result, sessions_delete_request, sessions_enrich_metadata_request, session_set_credentials_params, session_set_credentials_result, session_settings_built_in_tool_availability_snapshot, session_settings_evaluate_predicate_request, session_settings_evaluate_predicate_result, session_settings_job_snapshot, session_settings_model_snapshot, session_settings_online_evaluation_snapshot, session_settings_predicate_name, session_settings_repo_snapshot, session_settings_snapshot, session_settings_validation_snapshot, sessions_find_by_prefix_request, sessions_find_by_prefix_result, sessions_find_by_task_id_request, sessions_find_by_task_id_result, sessions_fork_request, sessions_fork_result, sessions_get_board_entry_count_request, sessions_get_board_entry_count_result, sessions_get_client_metadata_request, sessions_get_client_metadata_result, sessions_get_event_file_path_request, sessions_get_event_file_path_result, sessions_get_last_for_context_request, sessions_get_last_for_context_result, sessions_get_metadata_request, sessions_get_metadata_result, sessions_get_persisted_remote_steerable_request, sessions_get_persisted_remote_steerable_result, session_sizes, sessions_list_non_empty_session_ids_request, sessions_list_non_empty_session_ids_result, sessions_list_request, sessions_load_deferred_repo_hooks_request, sessions_open_attach, sessions_open_cloud, sessions_open_create, sessions_open_handoff, sessions_open_handoff_task_type, sessions_open_progress, sessions_open_progress_status, sessions_open_progress_step, sessions_open_remote, sessions_open_resume, sessions_open_resume_last, sessions_open_status, session_source, sessions_prune_old_request, sessions_read_persisted_events_request, sessions_release_lock_request, sessions_release_lock_result, sessions_reload_plugin_hooks_request, sessions_reload_plugin_hooks_result, sessions_save_request, sessions_save_result, sessions_set_additional_plugins_request, sessions_set_additional_plugins_result, sessions_set_remote_control_steering_request, sessions_start_remote_control_request, sessions_stop_remote_control_request, sessions_transfer_remote_control_request, session_telemetry_engagement, session_update_options_params, session_update_options_result, session_visibility_status, session_workflow_pause_at_checkpoint_result, session_working_directory_context, session_working_directory_context_host_type, settable_auth_info, settable_token_auth_info, shell_cancel_user_requested_request, shell_credentials, shell_exec_request, shell_exec_result, shell_execute_user_requested_request, shell_init_profile, shell_init_script, shell_init_script_shell, shell_kill_request, shell_kill_result, shell_kill_signal, shell_options, shutdown_request, skill, skill_apply_install_request, skill_apply_uninstall_request, skill_discovery_path, skill_discovery_path_list, skill_discovery_scope, skill_installation_failure_reason, skill_installation_file_review, skill_installation_location, skill_installation_management_outcome, skill_installation_management_result, skill_installation_operation_request, skill_installation_operation_status, skill_installation_outcome, skill_installation_ownership_state, skill_installation_result, skill_installation_review, skill_installation_scope, skill_installation_session_state, skill_installation_source, skill_installations_request, skill_installation_summary, skill_install_plan, skill_list, skill_plan_install_request, skill_plan_uninstall_request, skill_provider_descriptor, skill_provider_list_request, skill_provider_list_result, skill_provider_read_request, skill_provider_read_result, skills_config_set_disabled_skills_request, skills_config_set_skill_disabled_request, skills_disable_request, skills_discover_request, skills_enable_request, skill_set_enabled_request, skills_get_discovery_paths_request, skills_get_invoked_result, skills_invoked_skill, skills_load_diagnostics, skill_uninstall_plan, slash_command_add_timeline_entry_result, slash_command_agent_prompt_result, slash_command_completed_result, slash_command_info, slash_command_input, slash_command_input_choice, slash_command_input_completion, slash_command_invocation_result, slash_command_kind, slash_command_model_picker_dialog, slash_command_select_subcommand_option, slash_command_select_subcommand_result, slash_command_set_model_result, slash_command_set_plan_model_result, slash_command_show_dialog_result, slash_command_text_result, slash_command_timeline_entry, subagent_settings_entry, subagent_settings_entry_context_tier, system_message_block, task_agent_info, task_agent_progress, task_client_active_status, task_client_execution_mode, task_client_info, task_client_owner, task_client_owner_kind, task_client_owner_presence, task_client_progress, task_client_status, task_client_type, task_client_update, task_complete_data, task_completion_decision, task_execution_mode, task_info, task_kind, task_list, task_progress_line, tasks_cancel_request, tasks_cancel_result, tasks_get_current_promotable_result, tasks_get_progress_request, tasks_get_progress_result, task_shell_info, task_shell_info_attachment_mode, task_shell_progress, tasks_promote_current_to_background_result, tasks_promote_to_background_request, tasks_promote_to_background_result, tasks_refresh_result, tasks_register_request, tasks_register_result, tasks_remove_request, tasks_remove_result, tasks_send_message_request, tasks_send_message_result, tasks_start_agent_request, tasks_start_agent_result, task_status, tasks_update_request, tasks_update_result, tasks_wait_for_pending_result, telemetry_set_feature_overrides_request, token_auth_info, token_provider_auth_info, tool, tool_list, tool_result, tool_result_expanded, tool_result_new_message, tool_result_type, tools_execute_request, tools_get_builtin_descriptors_request, tools_get_builtin_descriptors_result, tools_get_current_metadata_result, tools_initialize_and_validate_result, tools_list_request, tools_set_request, tools_set_result, tools_shell_descriptor_config, tools_task_complete_event_data_request, tools_update_subagent_settings_result, ui_auto_mode_switch_response, ui_elicitation_array_any_of_field, ui_elicitation_array_any_of_field_items, ui_elicitation_array_any_of_field_items_any_of, ui_elicitation_array_enum_field, ui_elicitation_array_enum_field_items, ui_elicitation_field_value, ui_elicitation_request, ui_elicitation_response, ui_elicitation_response_action, ui_elicitation_response_content, ui_elicitation_result, ui_elicitation_schema, ui_elicitation_schema_property, ui_elicitation_schema_property_boolean, ui_elicitation_schema_property_number, ui_elicitation_schema_property_number_type, ui_elicitation_schema_property_string, ui_elicitation_schema_property_string_format, ui_elicitation_string_enum_field, ui_elicitation_string_one_of_field, ui_elicitation_string_one_of_field_one_of, ui_ephemeral_query_request, ui_ephemeral_query_result, ui_exit_plan_mode_action, ui_exit_plan_mode_response, ui_handle_pending_auto_mode_switch_request, ui_handle_pending_elicitation_request, ui_handle_pending_exit_plan_mode_request, ui_handle_pending_result, ui_handle_pending_sampling_request, ui_handle_pending_sampling_response, ui_handle_pending_session_limits_exhausted_request, ui_handle_pending_user_input_request, ui_register_direct_auto_mode_switch_handler_result, ui_session_limits_exhausted_response, ui_session_limits_exhausted_response_action, ui_unregister_direct_auto_mode_switch_handler_request, ui_unregister_direct_auto_mode_switch_handler_result, ui_user_input_response, unsupported_enqueue_command_result, update_subagent_settings_request, usage_get_metrics_result, usage_metrics_agent_metric, usage_metrics_code_changes, usage_metrics_model_metric, usage_metrics_model_metric_requests, usage_metrics_model_metric_token_detail, usage_metrics_model_metric_usage, usage_metrics_token_detail, user_auth_info, user_requested_shell_command_result, user_setting_metadata, user_settings_get_result, user_settings_set_request, user_settings_set_result, visibility_get_result, visibility_set_request, visibility_set_result, workflow_abort_request, workflow_ack_result, workflow_agent_options, workflow_agent_request, workflow_agent_result, workflow_agent_summary, workflow_cancel_request, workflow_current_phase, workflow_declared_limits, workflow_durable_operation, workflow_execute_request, workflow_execute_result, workflow_get_run_progress_request, workflow_get_run_request, workflow_journal_get_request, workflow_journal_get_result, workflow_journal_put_request, workflow_list_runs_request, workflow_list_runs_result, workflow_log_line, workflow_log_line_kind, workflow_log_request, workflow_pause_checkpoint_action, workflow_pause_checkpoint_request, workflow_pause_checkpoint_result, workflow_pause_info, workflow_pause_request, workflow_phase_observation, workflow_phase_status, workflow_progress_line, workflow_progress_page, workflow_resume_request, workflow_resume_result, workflow_run_consumed, workflow_run_detail, workflow_run_failure, workflow_run_failure_kind, workflow_run_limits, workflow_run_options, workflow_run_request, workflow_run_result, workflow_run_status, workflow_run_summary, workflow_run_terminal, workflow_tool_resume_request, workflow_tool_run_options, workflow_tool_run_request, workspace_diff_file_change, workspace_diff_file_change_type, workspace_diff_mode, workspace_diff_result, workspaces_add_summary_request, workspaces_add_summary_result, workspaces_autopilot_objective_exists_result, workspaces_checkpoints, workspaces_create_directory_request, workspaces_create_file_request, workspaces_delete_autopilot_objective_result, workspaces_diff_request, workspaces_ensure_request, workspaces_get_workspace_result, workspaces_list_checkpoints_result, workspaces_list_files_result, workspaces_read_autopilot_objective_result, workspaces_read_checkpoint_request, workspaces_read_checkpoint_result, workspaces_read_file_request, workspaces_read_file_result, workspaces_remove_path_request, workspaces_rename_path_request, workspaces_save_large_paste_request, workspaces_save_large_paste_result, workspaces_stat_file_request, workspaces_stat_file_result, workspaces_truncate_summaries_request, workspace_summary_host_type, workspaces_update_metadata_request, workspaces_workspace_details_host_type, workspaces_write_autopilot_objective_request, workspaces_write_autopilot_objective_result, connector_session_account_result, session_auth_info_result, session_context_attribution, session_context_info, subagent_settings, task_progress, workspace_summary) + return RPC(abort_request, abort_result, accepted_enqueue_command_result, account_all_users, account_auth_info, account_get_all_users_result, account_get_current_auth_result, account_get_quota_request, account_get_quota_result, account_kind, account_login_request, account_login_result, account_logout_request, account_logout_result, account_quota_snapshot, accounts_enumerate_request, accounts_get_request, accounts_set_request, account_status, adaptive_thinking_support, agent_discovery_path, agent_discovery_path_list, agent_discovery_path_scope, agent_get_current_result, agent_info, agent_info_source, agent_list, agent_list_request, agent_registry_live_target_entry, agent_registry_live_target_entry_attention_kind, agent_registry_live_target_entry_kind, agent_registry_live_target_entry_last_terminal_event, agent_registry_live_target_entry_status, agent_registry_log_capture, agent_registry_log_capture_open_error_reason, agent_registry_spawn_error, agent_registry_spawn_permission_mode, agent_registry_spawn_registry_timeout, agent_registry_spawn_request, agent_registry_spawn_result, agent_registry_spawn_spawned, agent_registry_spawn_validation_error, agent_registry_spawn_validation_error_field, agent_registry_spawn_validation_error_reason, agent_reload_result, agents_discover_request, agent_select_request, agent_select_result, agent_set_prompt_request, agents_get_discovery_paths_request, api_key_auth_info, auth_enumerate_query, auth_enumerate_value, auth_identity, auth_identity_metadata, auth_info, auth_info_type, auth_login_account, auth_login_advance_request, auth_login_begin_request, auth_login_begun, auth_login_cancel_request, auth_login_result_dto, auth_login_result_status, auth_login_step, auth_read_query, auth_read_value, auth_status_dto, auth_validation_error, auth_validation_errors, auth_write, auth_write_result, autopilot_objective_credit_limit, autopilot_objective_get_state_result, autopilot_objective_state, autopilot_objective_status, built_in_model_catalog, built_in_model_catalog_entry, builtin_tool_descriptor, builtin_tool_format, builtin_tool_format_type, builtin_tool_input_schema, builtin_tool_input_schema_type, builtin_tool_safe_for_telemetry, builtin_tool_safe_telemetry_fields, cancel_user_requested_shell_command_result, canvas_action, canvas_action_invoke_request, canvas_action_invoke_result, canvas_close_request, canvas_host_context, canvas_host_context_capabilities, canvas_json_schema, canvas_list, canvas_list_open_result, canvas_open_request, canvas_provider_close_request, canvas_provider_invoke_action_request, canvas_provider_open_request, canvas_provider_open_result, canvas_provider_register_request, canvas_provider_unregister_request, canvas_session_context, capi_session_options, card_digest, card_digest_algorithm, card_digest_value, catalog_agent_plugin_candidate, catalog_agent_plugin_candidate_kind, catalog_agent_plugin_candidate_provenance, catalog_agent_plugin_compatibility_tag, catalog_agent_plugin_media_type, catalog_ai_skill_candidate, catalog_ai_skill_candidate_kind, catalog_ai_skill_candidate_provenance, catalog_ai_skill_installability, catalog_ai_skill_media_type, catalog_authentication_required_error, catalog_authentication_required_reason, catalog_candidate, catalog_candidate_kind, catalog_candidate_source, catalog_candidate_source_embedded, catalog_candidate_source_url, catalog_capability, catalog_capability_id, catalog_client_contract, catalog_contract_violation_error, catalog_contract_violation_reason, catalog_handle_rejected_error, catalog_handle_rejection_reason, catalog_handle_type, catalog_invalid_request_error, catalog_invalid_request_field, catalog_malformed_card_error, catalog_malformed_card_reason, catalog_mcp_server_candidate, catalog_mcp_server_candidate_kind, catalog_mcp_server_candidate_provenance, catalog_mcp_server_installability, catalog_media_type, catalog_negotiated_contract, catalog_negotiation_refused_error, catalog_negotiation_refused_reason, catalog_network_failure_error, catalog_network_failure_reason, catalog_not_installable_error, catalog_not_installable_reason, catalog_plugin_repository_source, catalog_policy_rejected_error, catalog_resource_identity, catalog_resource_version, catalog_search_page, catalog_search_pagination, catalog_search_request, catalog_search_result, catalog_search_succeeded, catalog_search_total_count_relation, catalog_selection_cancelled, catalog_selection_decision, catalog_selection_declined, catalog_selection_foreign, catalog_selection_invalid, catalog_selection_replayed, catalog_selection_request, catalog_selection_result, catalog_selection_selected, catalog_selection_stale, catalog_selection_timed_out, catalog_selection_wrong_kind, catalog_trust_eligibility, catalog_trust_provenance, catalog_trust_snapshot, catalog_trust_snapshot_absent, catalog_trust_snapshot_absent_status, catalog_trust_snapshot_current, catalog_trust_snapshot_current_status, catalog_trust_snapshot_downgraded, catalog_trust_snapshot_downgraded_status, catalog_trust_snapshot_malformed, catalog_trust_snapshot_malformed_status, catalog_trust_snapshot_revoked, catalog_trust_snapshot_revoked_status, catalog_trust_snapshot_schema_version, catalog_trust_snapshot_stale, catalog_trust_snapshot_stale_status, catalog_trust_snapshot_unsupported, catalog_trust_snapshot_unsupported_status, catalog_trust_source, catalog_trust_tier, catalog_unavailable_error, catalog_unavailable_reason, catalog_unavailable_transport_error, catalog_unavailable_transport_reason, catalog_unsafe_retrieval_error, catalog_unsafe_retrieval_reason, catalog_unsupported_kind_error, client_metadata, client_task_cancel_reason, client_task_cancel_request, client_task_cancel_result, command_list, commands_finalize_invocation_effect_request, commands_finalize_invocation_effect_result, commands_handle_pending_command_request, commands_handle_pending_command_result, commands_invocation_effect_outcome, commands_invocation_origin, commands_invoke_request, commands_list_request, commands_respond_to_queued_command_request, commands_respond_to_queued_command_result, completions_get_trigger_characters_result, completions_request_request, completions_request_result, configure_session_extensions_params, connect_client_info, connected_remote_session_metadata, connected_remote_session_metadata_kind, connected_remote_session_metadata_repository, connector_account_request, connector_authorization_requirement, connector_authorization_scope, connector_availability, connector_capabilities, connector_catalog_entry, connector_catalog_result, connector_catalog_status, connector_connect_request, connector_connect_result, connector_continue_request, connector_disconnect_result, connector_mcp_status, connector_reconcile_request, connector_runtime_status, connector_session_account, connector_status, connect_remote_session_params, connect_request, connect_result, content_exclusion_check_paths_request, content_exclusion_check_paths_result, content_exclusion_path_check, content_filter_mode, context_heaviest_message, copilot_api_token_auth_info, copilot_user_response, copilot_user_response_endpoints, copilot_user_response_quota_snapshots, copilot_user_response_quota_snapshots_chat, copilot_user_response_quota_snapshots_completions, copilot_user_response_quota_snapshots_premium_interactions, current_model, current_tool_metadata, customization_reload_outcome, customization_reload_status, customization_reload_subsystem, customizations_reload_result, debug_collect_logs_collected_entry, debug_collect_logs_destination, debug_collect_logs_entry, debug_collect_logs_entry_kind, debug_collect_logs_include, debug_collect_logs_redaction, debug_collect_logs_request, debug_collect_logs_result, debug_collect_logs_result_kind, debug_collect_logs_skipped_entry, debug_collect_logs_source, diagnostic_cursor_status, diagnostic_entry, diagnostic_log_level, diagnostics_configuration, diagnostics_configure_request, diagnostic_severity, diagnostic_source, diagnostic_sources_configuration, diagnostics_read_request, diagnostics_read_result, discovered_canvas, discovered_extension, discovered_extension_mode, discovered_extension_plugin, discovered_extensions, discovered_extensions_disable_request, discovered_extensions_enable_request, discovered_extension_source, discovered_hook, discovered_mcp_server, discovered_mcp_server_type, discovered_model, discovered_model_list, enqueue_command_params, enqueue_command_result, env_auth_info, environment_capabilities, environment_kind, environments_delete_request, environments_delete_result, environments_get_request, environments_get_result, environments_list_request, environments_list_result, event_log_read_request, event_log_release_interest_result, event_log_tail_result, event_log_types, events_agent_scope, events_cursor_status, events_read_direction, events_read_result, execute_command_params, execute_command_result, extension, extension_context_push_input, extension_launch_profile, extension_launch_provider_resolve_request, extension_launch_provider_resolve_result, extension_list, extensions_disable_request, extensions_enable_request, extension_source, extension_status, external_tool_result, external_tool_text_result_for_llm, external_tool_text_result_for_llm_binary_results_for_llm, external_tool_text_result_for_llm_binary_results_for_llm_type, external_tool_text_result_for_llm_content, external_tool_text_result_for_llm_content_audio, external_tool_text_result_for_llm_content_image, external_tool_text_result_for_llm_content_resource, external_tool_text_result_for_llm_content_resource_details, external_tool_text_result_for_llm_content_resource_link, external_tool_text_result_for_llm_content_resource_link_icon, external_tool_text_result_for_llm_content_resource_link_icon_theme, external_tool_text_result_for_llm_content_shell_exit, external_tool_text_result_for_llm_content_terminal, external_tool_text_result_for_llm_content_text, filter_mapping, fleet_start_request, fleet_start_result, folder_trust_add_params, folder_trust_check_params, folder_trust_check_result, gh_cli_auth_info, git_hub_environment, git_hub_telemetry_client_info, git_hub_telemetry_event, git_hub_telemetry_notification, git_hub_token_acquire_reason, git_hub_token_acquire_request, git_hub_token_acquire_result, handle_pending_tool_call_request, handle_pending_tool_call_result, history_abort_manual_compaction_result, history_cancel_background_compaction_result, history_clear_context_request, history_clear_context_result, history_compact_context_window, history_compact_request, history_compact_result, history_file_restore_skip_reason, history_list_rewind_points_result, history_preview_rewind_request, history_preview_rewind_result, history_rewind_change_type, history_rewind_file_preview, history_rewind_mode, history_rewind_outcome, history_rewind_point, history_rewind_request, history_rewind_result, history_rewind_unavailable_reason, history_skipped_file_restore, history_summarize_for_handoff_result, history_truncate_request, history_truncate_result, hmac_auth_info, hook_invoke_request, hook_invoke_response, hook_origin, hooks_discover_request, hooks_discover_result, hook_type, host_configuration, host_dispose_request, host_empty_result, host_environment_credentials, host_exited_notification, host_exit_reason, host_git_hub_environment_options, host_local_server_configuration, host_local_server_options, host_publish_session_request, host_publish_session_result, host_ready_request, host_register_session_request, host_session_create_callback, host_session_create_request, host_session_create_result, host_session_released_notification, host_session_release_request, host_start_request, host_start_result, installation_catalogue_identity, installation_confirmation_request, installation_confirmation_response, installation_decision, installation_review, installed_plugin, installed_plugin_info, installed_plugin_source, installed_plugin_source_git_hub, installed_plugin_source_local, installed_plugin_source_url, instruction_discovery_path, instruction_discovery_path_kind, instruction_discovery_path_list, instruction_discovery_path_location, instructions_discover_request, instructions_get_discovery_paths_request, instructions_get_sources_result, instruction_source, instruction_source_location, instruction_source_type, interrupt_main_turn_request, interrupt_main_turn_result, json_schema_response_format, llm_inference_headers, llm_inference_http_request_chunk_request, llm_inference_http_request_chunk_result, llm_inference_http_request_start_request, llm_inference_http_request_start_result, llm_inference_http_request_start_transport, llm_inference_http_response_chunk_error, llm_inference_http_response_chunk_request, llm_inference_http_response_chunk_result, llm_inference_http_response_start_request, llm_inference_http_response_start_result, llm_inference_set_provider_result, local_session_metadata_value, login_provider_kind, log_request, log_result, lsp_initialize_request, managed_mcp_server_config, managed_setting_meta, managed_settings_channel, managed_settings_compose_layer, managed_settings_compose_request, managed_settings_compose_result, managed_settings_diagnostic, managed_settings_diagnostic_severity, managed_settings_layer, managed_settings_meta, managed_settings_read_result, managed_settings_resolved_data, managed_settings_resolve_request, managed_settings_resolve_result, managed_settings_schema_result, managed_settings_validate_request, managed_settings_validate_result, managed_settings_values, marketplace_add_result, marketplace_browse_result, marketplace_info, marketplace_list_result, marketplace_plugin_info, marketplace_refresh_entry, marketplace_refresh_result, marketplace_remove_result, mcp_allowed_server, mcp_apply_install_request, mcp_apply_uninstall_request, mcp_apps_call_tool_request, mcp_apps_diagnose_capability, mcp_apps_diagnose_request, mcp_apps_diagnose_result, mcp_apps_diagnose_server, mcp_apps_host_context, mcp_apps_host_context_details, mcp_apps_host_context_details_available_display_mode, mcp_apps_host_context_details_display_mode, mcp_apps_host_context_details_platform, mcp_apps_host_context_details_theme, mcp_apps_list_tools_request, mcp_apps_list_tools_result, mcp_apps_read_resource_request, mcp_apps_read_resource_result, mcp_apps_resource_content, mcp_apps_set_host_context_details, mcp_apps_set_host_context_details_available_display_mode, mcp_apps_set_host_context_details_display_mode, mcp_apps_set_host_context_details_platform, mcp_apps_set_host_context_details_theme, mcp_apps_set_host_context_request, mcp_cancel_sampling_execution_params, mcp_cancel_sampling_execution_result, mcp_config_add_request, mcp_config_disable_request, mcp_config_enable_request, mcp_config_list, mcp_config_remove_request, mcp_config_update_request, mcp_configured_server, mcp_configured_server_list, mcp_configured_server_state, mcp_configure_git_hub_request, mcp_configure_git_hub_result, mcp_diagnostic_details, mcp_diagnostic_direction, mcp_diagnostic_kind, mcp_diagnostic_source_configuration, mcp_disable_request, mcp_discover_request, mcp_discover_result, mcp_elicitation_form_mode, mcp_enable_request, mcp_execute_sampling_params, mcp_execute_sampling_request, mcp_execute_sampling_result, mcp_failed_server, mcp_filtered_server, mcp_headers_handle_pending_headers_refresh_request, mcp_headers_handle_pending_headers_refresh_request_request, mcp_headers_handle_pending_headers_refresh_request_result, mcp_host_state, mcp_installation_failure_reason, mcp_installation_input, mcp_installation_management_outcome, mcp_installation_management_result, mcp_installation_operation_request, mcp_installation_operation_status, mcp_installation_outcome, mcp_installation_remote_configuration, mcp_installation_result, mcp_installation_review, mcp_installation_secret, mcp_installation_secret_storage, mcp_installations_request, mcp_installation_state, mcp_installation_summary, mcp_install_plan, mcp_is_server_running_request, mcp_is_server_running_result, mcp_list_tools_request, mcp_list_tools_result, mcp_oauth_authentication_state_changed_request, mcp_oauth_cancel_login_request, mcp_oauth_cancel_login_result, mcp_oauth_complete_request, mcp_oauth_handle_pending_request, mcp_oauth_handle_pending_result, mcp_oauth_login_grant_type, mcp_oauth_login_request, mcp_oauth_login_result, mcp_oauth_pending_request_response, mcp_oauth_prepare_login_request, mcp_oauth_prepare_login_result, mcp_oauth_probe_needs_auth_reason, mcp_oauth_probe_request, mcp_oauth_probe_result, mcp_oauth_respond_request, mcp_oauth_respond_result, mcp_owned_oauth_login_status, mcp_plan_configuration_change, mcp_plan_configuration_operation, mcp_plan_enum_value_type, mcp_plan_install_planned, mcp_plan_install_request, mcp_plan_install_result, mcp_plan_install_source, mcp_plan_install_source_candidate, mcp_plan_install_source_candidate_kind, mcp_plan_install_source_card, mcp_plan_install_source_card_kind, mcp_plan_package_install_method, mcp_plan_package_transport, mcp_plan_policy_decision, mcp_plan_policy_result, mcp_plan_policy_source, mcp_plan_provenance, mcp_plan_remote_install_method, mcp_plan_remote_transport, mcp_plan_required_value, mcp_plan_required_value_enum, mcp_plan_required_value_enum_kind, mcp_plan_required_value_scalar, mcp_plan_required_value_scalar_kind, mcp_plan_resource_identity, mcp_plan_scalar_value_type, mcp_plan_scope, mcp_plan_secret_placeholder, mcp_plan_secret_reference, mcp_plan_target, mcp_plan_transport_choice, mcp_plan_transport_choice_package, mcp_plan_transport_choice_remote, mcp_plan_uninstall_request, mcp_plan_value_category, mcp_prepared_install, mcp_prepare_install_request, mcp_prompt, mcp_prompt_argument, mcp_prompt_icon, mcp_prompt_message, mcp_prompt_role, mcp_prompts_get_request, mcp_prompts_get_result, mcp_prompts_list_request, mcp_prompts_list_result, mcp_register_external_client_request, mcp_reload_config, mcp_reload_with_config_request, mcp_remove_git_hub_result, mcp_resource, mcp_resource_annotations, mcp_resource_content, mcp_resource_icon, mcp_resources_list_request, mcp_resources_list_result, mcp_resources_list_templates_request, mcp_resources_list_templates_result, mcp_resources_read_request, mcp_resources_read_result, mcp_resource_template, mcp_restart_server_request, mcp_safe_for_telemetry, mcp_safe_for_telemetry_fields, mcp_sampling_execution_action, mcp_sampling_execution_result, mcp_serializable_server_config, mcp_server, mcp_server_auth_config, mcp_server_auth_config_redirect_port, mcp_server_card_embedded, mcp_server_card_embedded_kind, mcp_server_card_media_type, mcp_server_card_reference, mcp_server_card_url, mcp_server_card_url_kind, mcp_server_config, mcp_server_config_defer_tools, mcp_server_config_http, mcp_server_config_http_oauth_grant_type, mcp_server_config_http_type, mcp_server_config_memory, mcp_server_config_memory_type, mcp_server_config_stdio, mcp_server_config_stdio_type, mcp_server_failure_info, mcp_server_list, mcp_server_needs_auth_info, mcp_server_ownership, mcp_set_env_value_mode_details, mcp_set_env_value_mode_params, mcp_set_env_value_mode_result, mcp_source_file, mcp_source_plugin, mcp_source_ref, mcp_start_server_request, mcp_start_servers_result, mcp_stop_server_request, mcp_task_metadata, mcp_tools, mcp_tool_ui, mcp_tool_ui_visibility, mcp_uninstall_plan, mcp_unregister_external_client_request, memory_configuration, metadata_context_attribution_result, metadata_context_heaviest_messages_request, metadata_context_heaviest_messages_result, metadata_context_info_request, metadata_context_info_result, metadata_is_processing_result, metadata_recompute_context_tokens_request, metadata_recompute_context_tokens_result, metadata_record_context_change_request, metadata_record_context_change_result, metadata_set_working_directory_request, metadata_set_working_directory_result, metadata_snapshot_current_mode, metadata_snapshot_remote_metadata, metadata_snapshot_remote_metadata_repository, metadata_snapshot_remote_metadata_task_type, metadata_update_client_metadata_request, model, model_apply_startup_overlay_request, model_artifact_details, model_billing, model_billing_promo, model_billing_token_prices, model_billing_token_prices_long_context, model_capabilities, model_capabilities_limits, model_capabilities_limits_vision, model_capabilities_override, model_capabilities_override_limits, model_capabilities_override_limits_vision, model_capabilities_override_supports, model_capabilities_supports, model_list, model_list_request, model_message, model_picker_category, model_picker_persistence_request, model_picker_price_category, model_picker_settings_context, model_policy, model_policy_state, model_provider_adapter_catalog, model_provider_adapter_descriptor, model_provider_adapter_operation_descriptor, model_provider_attribution, model_provider_automatic_discovery_mode, model_provider_automatic_discovery_policy, model_provider_configuration_disposition, model_provider_configuration_plan, model_provider_descriptor, model_provider_discover_request, model_provider_discover_result, model_provider_discovery_network_scope, model_provider_get_status_request, model_provider_instance, model_provider_instance_reference, model_provider_kind, model_provider_models_list_request, model_provider_operation_outcome, model_provider_operation_outcome_code, model_provider_prepare_configuration_request, model_provider_provenance, model_provider_provenance_source, model_provider_ref, model_provider_status, model_provider_warning, model_set_allowed_models_request, model_set_allowed_models_result, model_set_reasoning_effort_request, model_set_reasoning_effort_result, models_list_request, model_switch_auto_tier_request, model_switch_auto_tier_result, model_switch_auto_tier_status, model_switch_confirmation, model_switch_to_request, model_switch_to_result, model_warning_text, mode_set_request, mode_set_result, move_mcp_loading_to_background_result, named_provider_config, name_get_result, name_set_auto_request, name_set_auto_result, name_set_request, open_canvas_instance, options_update_additional_content_exclusion_policy, options_update_additional_content_exclusion_policy_rule, options_update_additional_content_exclusion_policy_rule_source, options_update_additional_content_exclusion_policy_scope, options_update_context_tier, options_update_env_value_mode, options_update_reasoning_summary, options_update_tool_filter_precedence, pending_permission_request, pending_permission_request_list, permission_decision, permission_decision_approved, permission_decision_approved_for_location, permission_decision_approved_for_session, permission_decision_approve_for_location, permission_decision_approve_for_location_approval, permission_decision_approve_for_location_approval_commands, permission_decision_approve_for_location_approval_custom_tool, permission_decision_approve_for_location_approval_extension_env_access, permission_decision_approve_for_location_approval_extension_management, permission_decision_approve_for_location_approval_extension_permission_access, permission_decision_approve_for_location_approval_mcp, permission_decision_approve_for_location_approval_mcp_sampling, permission_decision_approve_for_location_approval_memory, permission_decision_approve_for_location_approval_read, permission_decision_approve_for_location_approval_workflow, permission_decision_approve_for_location_approval_write, permission_decision_approve_for_session, permission_decision_approve_for_session_approval, permission_decision_approve_for_session_approval_commands, permission_decision_approve_for_session_approval_custom_tool, permission_decision_approve_for_session_approval_extension_env_access, permission_decision_approve_for_session_approval_extension_management, permission_decision_approve_for_session_approval_extension_permission_access, permission_decision_approve_for_session_approval_mcp, permission_decision_approve_for_session_approval_mcp_sampling, permission_decision_approve_for_session_approval_memory, permission_decision_approve_for_session_approval_read, permission_decision_approve_for_session_approval_workflow, permission_decision_approve_for_session_approval_write, permission_decision_approve_once, permission_decision_approve_permanently, permission_decision_approve_read_only_for_session, permission_decision_cancelled, permission_decision_context, permission_decision_denied_by_content_exclusion_policy, permission_decision_denied_by_permission_request_hook, permission_decision_denied_by_rules, permission_decision_denied_interactively_by_user, permission_decision_denied_no_approval_rule_and_could_not_request_from_user, permission_decision_outcome, permission_decision_reject, permission_decision_request, permission_decision_surface, permission_decision_user_not_available, permission_location_add_tool_approval_params, permission_location_apply_params, permission_location_apply_result, permission_location_resolve_params, permission_location_resolve_result, permission_location_type, permission_mode_source, permission_paths_add_params, permission_paths_allowed_check_params, permission_paths_allowed_check_result, permission_paths_config, permission_paths_list, permission_paths_update_primary_params, permission_paths_workspace_check_params, permission_paths_workspace_check_result, permission_prompt_shown_notification, permission_request_result, permission_response_capability, permission_rules_set, permissions_configure_additional_content_exclusion_policy, permissions_configure_additional_content_exclusion_policy_rule, permissions_configure_additional_content_exclusion_policy_rule_source, permissions_configure_additional_content_exclusion_policy_scope, permissions_configure_params, permissions_configure_result, permissions_folder_trust_add_trusted_result, permissions_get_mode_request, permissions_get_mode_result, permissions_locations_add_tool_approval_details, permissions_locations_add_tool_approval_details_commands, permissions_locations_add_tool_approval_details_custom_tool, permissions_locations_add_tool_approval_details_extension_env_access, permissions_locations_add_tool_approval_details_extension_management, permissions_locations_add_tool_approval_details_extension_permission_access, permissions_locations_add_tool_approval_details_mcp, permissions_locations_add_tool_approval_details_mcp_sampling, permissions_locations_add_tool_approval_details_memory, permissions_locations_add_tool_approval_details_read, permissions_locations_add_tool_approval_details_workflow, permissions_locations_add_tool_approval_details_write, permissions_locations_add_tool_approval_result, permissions_modify_rules_params, permissions_modify_rules_result, permissions_modify_rules_scope, permissions_notify_prompt_shown_result, permissions_paths_add_result, permissions_paths_list_request, permissions_paths_update_primary_result, permissions_pending_requests_request, permissions_reset_session_approvals_request, permissions_reset_session_approvals_result, permissions_set_approve_all_request, permissions_set_approve_all_result, permissions_set_approve_all_source, permissions_set_mode_request, permissions_set_mode_result, permissions_set_required_request, permissions_set_required_result, permissions_urls_set_unrestricted_mode_result, permission_urls_config, permission_urls_set_unrestricted_mode_params, ping_request, ping_result, plan_read_result, plan_read_sql_todos_result, plan_read_sql_todos_with_dependencies_result, plan_sql_todo_dependency, plan_sql_todos_row, plan_update_request, plugin, plugin_install_result, plugin_install_staging_mode, plugin_list, plugin_list_result, plugins_builtin_set_request, plugins_disable_request, plugins_enable_request, plugins_install_request, plugins_marketplaces_add_request, plugins_marketplaces_browse_request, plugins_marketplaces_refresh_request, plugins_marketplaces_remove_request, plugins_reload_request, plugins_uninstall_request, plugins_update_request, plugin_update_all_entry, plugin_update_all_result, plugin_update_result, protocol_append_mode, protocol_customize_mode, protocol_external_tool_defer, protocol_external_tool_definition, protocol_marker_section_override, protocol_replace_mode, protocol_section_override, protocol_static_section_action, protocol_static_section_override, protocol_system_message_append_config, protocol_system_message_config, protocol_system_message_customize_config, protocol_system_message_replace_config, provider_add_request, provider_add_result, provider_config, provider_config_azure, provider_config_model_provider, provider_config_transport, provider_config_type, provider_config_wire_api, provider_descriptor, provider_endpoint, provider_endpoint_transport, provider_endpoint_type, provider_endpoint_wire_api, provider_get_endpoint_request, provider_model_config, provider_session_token, provider_sync_request, provider_sync_result, provider_token_acquire_request, provider_token_acquire_result, provider_withdraw_request, provider_withdraw_result, push_attachment, push_attachment_blob, push_attachment_directory, push_attachment_file, push_attachment_file_line_range, push_attachment_git_hub_actions_job, push_attachment_git_hub_commit, push_attachment_git_hub_file, push_attachment_git_hub_file_diff, push_attachment_git_hub_file_diff_side, push_attachment_git_hub_reference, push_attachment_git_hub_reference_type, push_attachment_git_hub_release, push_attachment_git_hub_repository, push_attachment_git_hub_snippet, push_attachment_git_hub_tree_comparison, push_attachment_git_hub_tree_comparison_side, push_attachment_git_hub_url, push_attachment_selection, push_attachment_selection_details, push_attachment_selection_details_end, push_attachment_selection_details_start, push_git_hub_repo_ref, queue_append_steering_request, queue_begin_deferred_idle_drain_request, queue_begin_deferred_idle_drain_result, queue_consume_system_notifications_request, queued_command_handled, queued_command_not_handled, queued_command_result, queue_defer_session_idle_request, queue_duplicate_at_request, queue_duplicate_at_result, queue_enqueue_resume_pending_result, queue_finish_deferred_idle_drain_request, queue_finish_deferred_idle_drain_result, queue_has_pending_result, queue_insert_at_request, queue_insert_at_result, queue_insert_message, queue_move_item_request, queue_move_item_result, queue_pending_items, queue_pending_items_kind, queue_pending_items_result, queue_remove_at_request, queue_remove_at_result, queue_remove_most_recent_result, queue_send_now_request, queue_send_now_result, queue_set_drain_paused_request, queue_snapshot_result, queue_update_text_request, queue_update_text_result, queue_withdraw_message_request, queue_withdraw_message_result, register_event_interest_params, register_event_interest_result, release_event_interest_params, remote_control_config, remote_control_config_existing_mc_session, remote_control_status, remote_control_status_active, remote_control_status_connecting, remote_control_status_error, remote_control_status_off, remote_control_status_result, remote_control_stop_result, remote_control_transfer_result, remote_enable_request, remote_enable_result, remote_notify_steerable_changed_request, remote_notify_steerable_changed_result, remote_session_connection_result, remote_session_host_status, remote_session_metadata_repository, remote_session_metadata_task_type, remote_session_metadata_value, remote_session_mode, remote_session_repository, response_format, sandbox_config, sandbox_config_auth, sandbox_config_source, sandbox_config_user_policy, sandbox_config_user_policy_experimental, sandbox_config_user_policy_experimental_seatbelt, sandbox_config_user_policy_filesystem, sandbox_config_user_policy_network, sandbox_config_user_policy_network_proxy, sandbox_config_user_policy_seatbelt, sandbox_credentials_config, sandbox_disable_for_session_request, sandbox_disable_for_session_result, sandbox_enforcement_status, sandbox_grant_path_for_request_request, sandbox_grant_path_for_request_result, sandbox_host_capability, sandbox_host_capability_name, sandbox_host_support, sandbox_masked_env_var, sandbox_proxy_ca_create_result, sandbox_proxy_ca_request, sandbox_proxy_ca_state, sandbox_proxy_ca_status, sandbox_session_change, schedule_add_at_request, schedule_add_cron_request, schedule_add_request, schedule_add_result, schedule_add_self_paced_request, schedule_entry, schedule_has_self_paced_result, schedule_list, schedule_rearm_self_paced_request, schedule_stop_request, schedule_stop_result, secrets_add_filter_values_request, secrets_add_filter_values_result, send_agent_mode, send_attachments_to_message_params, send_message_item, send_messages_request, send_messages_result, send_mode, send_request, send_result, send_system_notification_request, server_agent_list, server_instruction_source_list, server_skill, server_skill_list, session_activity, session_agent_list_request, session_auth_login_request, session_auth_logout_user_request, session_auth_status, session_auth_switch_request, session_bulk_delete_result, session_cancel_all_background_agents_result, session_capability, session_commands_list_request, session_completion_item, session_context, session_context_host_type, session_enrich_metadata_result, session_fs_append_file_request, session_fs_error, session_fs_error_code, session_fs_exists_request, session_fs_exists_result, session_fs_mkdir_request, session_fs_readdir_request, session_fs_readdir_result, session_fs_readdir_with_types_entry, session_fs_readdir_with_types_entry_type, session_fs_readdir_with_types_request, session_fs_readdir_with_types_result, session_fs_read_file_bytes_request, session_fs_read_file_bytes_result, session_fs_read_file_request, session_fs_read_file_result, session_fs_rename_request, session_fs_rm_request, session_fs_set_provider_capabilities, session_fs_set_provider_conventions, session_fs_set_provider_request, session_fs_set_provider_result, session_fs_sqlite_exists_request, session_fs_sqlite_exists_result, session_fs_sqlite_query_request, session_fs_sqlite_query_result, session_fs_sqlite_query_type, session_fs_sqlite_transaction_error, session_fs_sqlite_transaction_error_class, session_fs_sqlite_transaction_request, session_fs_sqlite_transaction_result, session_fs_sqlite_transaction_statement, session_fs_stat_request, session_fs_stat_result, session_fs_write_file_bytes_request, session_fs_write_file_request, session_git_hub_auth_get_all_auth_available_result, session_git_hub_auth_logout_result, session_git_hub_auth_logout_user_result, session_history_compact_request, session_installed_plugin, session_installed_plugin_source, session_installed_plugin_source_git_hub, session_installed_plugin_source_local, session_installed_plugin_source_url, session_limit_prediction_baseline_data, session_limit_prediction_client_type, session_limit_prediction_details, session_limit_prediction_predict_request, session_limit_prediction_request, session_limit_prediction_result, session_limit_prediction_source, session_limit_prediction_tier, session_limit_prediction_tier_option, session_limit_prediction_unavailable_reason, session_list, session_list_entry, session_list_filter, session_load_deferred_repo_hooks_result, session_log_level, session_managed_permissions, session_managed_settings, session_mcp_apps_call_tool_result, session_mcp_oauth_cancel_login_request, session_mcp_oauth_cancel_login_result, session_mcp_oauth_prepare_login_request, session_mcp_oauth_prepare_login_result, session_metadata_snapshot, session_mode, session_model_list, session_model_list_request, session_model_price_category, session_open_options, session_open_options_additional_content_exclusion_policy, session_open_options_additional_content_exclusion_policy_rule, session_open_options_additional_content_exclusion_policy_rule_source, session_open_options_additional_content_exclusion_policy_scope, session_open_options_env_value_mode, session_open_options_reasoning_summary, session_open_params, session_open_result, session_plugins_disable_request, session_plugins_enable_request, session_plugins_install_request, session_plugins_marketplaces_refresh_request, session_plugins_reload_request, session_provider_get_endpoint_request, session_prune_result, sessions_bulk_delete_request, sessions_check_in_use_request, sessions_check_in_use_result, sessions_client_metadata_entry, sessions_close_request, sessions_close_result, sessions_delete_request, sessions_enrich_metadata_request, session_set_credentials_params, session_set_credentials_result, session_settings_built_in_tool_availability_snapshot, session_settings_evaluate_predicate_request, session_settings_evaluate_predicate_result, session_settings_job_snapshot, session_settings_model_snapshot, session_settings_online_evaluation_snapshot, session_settings_predicate_name, session_settings_repo_snapshot, session_settings_snapshot, session_settings_validation_snapshot, sessions_find_by_prefix_request, sessions_find_by_prefix_result, sessions_find_by_task_id_request, sessions_find_by_task_id_result, sessions_fork_request, sessions_fork_result, sessions_get_board_entry_count_request, sessions_get_board_entry_count_result, sessions_get_client_metadata_request, sessions_get_client_metadata_result, sessions_get_event_file_path_request, sessions_get_event_file_path_result, sessions_get_last_for_context_request, sessions_get_last_for_context_result, sessions_get_metadata_request, sessions_get_metadata_result, sessions_get_persisted_remote_steerable_request, sessions_get_persisted_remote_steerable_result, session_sizes, sessions_list_non_empty_session_ids_request, sessions_list_non_empty_session_ids_result, sessions_list_request, sessions_load_deferred_repo_hooks_request, sessions_open_attach, sessions_open_cloud, sessions_open_create, sessions_open_handoff, sessions_open_handoff_task_type, sessions_open_progress, sessions_open_progress_status, sessions_open_progress_step, sessions_open_remote, sessions_open_resume, sessions_open_resume_last, sessions_open_status, session_source, sessions_prune_old_request, sessions_read_persisted_events_request, sessions_release_lock_request, sessions_release_lock_result, sessions_reload_plugin_hooks_request, sessions_reload_plugin_hooks_result, sessions_save_request, sessions_save_result, sessions_set_additional_plugins_request, sessions_set_additional_plugins_result, sessions_set_remote_control_steering_request, sessions_start_remote_control_request, sessions_stop_remote_control_request, sessions_transfer_remote_control_request, session_telemetry_engagement, session_update_options_params, session_update_options_result, session_visibility_status, session_workflow_pause_at_checkpoint_result, session_working_directory_context, session_working_directory_context_host_type, settable_auth_info, settable_token_auth_info, shell_cancel_user_requested_request, shell_credentials, shell_exec_request, shell_exec_result, shell_execute_user_requested_request, shell_init_profile, shell_init_script, shell_init_script_shell, shell_kill_request, shell_kill_result, shell_kill_signal, shell_options, shutdown_request, skill, skill_apply_install_request, skill_apply_uninstall_request, skill_discovery_path, skill_discovery_path_list, skill_discovery_scope, skill_installation_failure_reason, skill_installation_file_review, skill_installation_location, skill_installation_management_outcome, skill_installation_management_result, skill_installation_operation_request, skill_installation_operation_status, skill_installation_outcome, skill_installation_ownership_state, skill_installation_result, skill_installation_review, skill_installation_scope, skill_installation_session_state, skill_installation_source, skill_installations_request, skill_installation_summary, skill_install_plan, skill_list, skill_plan_install_request, skill_plan_uninstall_request, skill_provider_descriptor, skill_provider_list_request, skill_provider_list_result, skill_provider_read_request, skill_provider_read_result, skills_config_set_disabled_skills_request, skills_config_set_skill_disabled_request, skills_disable_request, skills_discover_request, skills_enable_request, skill_set_enabled_request, skills_get_discovery_paths_request, skills_get_invoked_result, skills_invoked_skill, skills_load_diagnostics, skill_uninstall_plan, slash_command_add_timeline_entry_result, slash_command_agent_prompt_result, slash_command_completed_result, slash_command_info, slash_command_input, slash_command_input_choice, slash_command_input_completion, slash_command_invocation_result, slash_command_kind, slash_command_model_picker_dialog, slash_command_select_subcommand_option, slash_command_select_subcommand_result, slash_command_set_model_result, slash_command_set_plan_model_result, slash_command_show_dialog_result, slash_command_text_result, slash_command_timeline_entry, subagent_settings_entry, subagent_settings_entry_context_tier, system_message_block, task_agent_info, task_agent_progress, task_client_active_status, task_client_execution_mode, task_client_info, task_client_owner, task_client_owner_kind, task_client_owner_presence, task_client_progress, task_client_status, task_client_type, task_client_update, task_complete_data, task_completion_decision, task_execution_mode, task_info, task_kind, task_list, task_progress_line, tasks_cancel_request, tasks_cancel_result, tasks_get_current_promotable_result, tasks_get_progress_request, tasks_get_progress_result, task_shell_info, task_shell_info_attachment_mode, task_shell_progress, tasks_promote_current_to_background_result, tasks_promote_to_background_request, tasks_promote_to_background_result, tasks_refresh_result, tasks_register_request, tasks_register_result, tasks_remove_request, tasks_remove_result, tasks_send_message_request, tasks_send_message_result, tasks_start_agent_request, tasks_start_agent_result, task_status, tasks_update_request, tasks_update_result, tasks_wait_for_pending_result, telemetry_set_feature_overrides_request, token_auth_info, token_provider_auth_info, tool, tool_list, tool_result, tool_result_expanded, tool_result_new_message, tool_result_type, tools_execute_request, tools_get_builtin_descriptors_request, tools_get_builtin_descriptors_result, tools_get_current_metadata_result, tools_initialize_and_validate_result, tools_list_request, tools_set_request, tools_set_result, tools_shell_descriptor_config, tools_task_complete_event_data_request, tools_update_subagent_settings_result, ui_auto_mode_switch_response, ui_elicitation_array_any_of_field, ui_elicitation_array_any_of_field_items, ui_elicitation_array_any_of_field_items_any_of, ui_elicitation_array_enum_field, ui_elicitation_array_enum_field_items, ui_elicitation_field_value, ui_elicitation_request, ui_elicitation_response, ui_elicitation_response_action, ui_elicitation_response_content, ui_elicitation_result, ui_elicitation_schema, ui_elicitation_schema_property, ui_elicitation_schema_property_boolean, ui_elicitation_schema_property_number, ui_elicitation_schema_property_number_type, ui_elicitation_schema_property_string, ui_elicitation_schema_property_string_format, ui_elicitation_string_enum_field, ui_elicitation_string_one_of_field, ui_elicitation_string_one_of_field_one_of, ui_ephemeral_query_request, ui_ephemeral_query_result, ui_exit_plan_mode_action, ui_exit_plan_mode_response, ui_handle_pending_auto_mode_switch_request, ui_handle_pending_elicitation_request, ui_handle_pending_exit_plan_mode_request, ui_handle_pending_result, ui_handle_pending_sampling_request, ui_handle_pending_sampling_response, ui_handle_pending_session_limits_exhausted_request, ui_handle_pending_user_input_request, ui_register_direct_auto_mode_switch_handler_result, ui_session_limits_exhausted_response, ui_session_limits_exhausted_response_action, ui_unregister_direct_auto_mode_switch_handler_request, ui_unregister_direct_auto_mode_switch_handler_result, ui_user_input_response, unsupported_enqueue_command_result, update_subagent_settings_request, usage_get_metrics_result, usage_metrics_agent_metric, usage_metrics_code_changes, usage_metrics_model_metric, usage_metrics_model_metric_requests, usage_metrics_model_metric_token_detail, usage_metrics_model_metric_usage, usage_metrics_token_detail, user_auth_info, user_requested_shell_command_result, user_setting_metadata, user_settings_get_result, user_settings_set_request, user_settings_set_result, visibility_get_result, visibility_set_request, visibility_set_result, workflow_abort_request, workflow_ack_result, workflow_agent_options, workflow_agent_request, workflow_agent_result, workflow_agent_summary, workflow_cancel_request, workflow_current_phase, workflow_declared_limits, workflow_durable_operation, workflow_execute_request, workflow_execute_result, workflow_get_run_progress_request, workflow_get_run_request, workflow_journal_get_request, workflow_journal_get_result, workflow_journal_put_request, workflow_list_runs_request, workflow_list_runs_result, workflow_log_line, workflow_log_line_kind, workflow_log_request, workflow_pause_checkpoint_action, workflow_pause_checkpoint_request, workflow_pause_checkpoint_result, workflow_pause_info, workflow_pause_request, workflow_phase_observation, workflow_phase_status, workflow_progress_line, workflow_progress_page, workflow_resume_request, workflow_resume_result, workflow_run_consumed, workflow_run_detail, workflow_run_failure, workflow_run_failure_kind, workflow_run_limits, workflow_run_options, workflow_run_request, workflow_run_result, workflow_run_status, workflow_run_summary, workflow_run_terminal, workflow_tool_resume_request, workflow_tool_run_options, workflow_tool_run_request, workspace_diff_file_change, workspace_diff_file_change_type, workspace_diff_mode, workspace_diff_result, workspaces_add_summary_request, workspaces_add_summary_result, workspaces_autopilot_objective_exists_result, workspaces_checkpoints, workspaces_create_directory_request, workspaces_create_file_request, workspaces_delete_autopilot_objective_result, workspaces_diff_request, workspaces_ensure_request, workspaces_get_workspace_result, workspaces_list_checkpoints_result, workspaces_list_files_result, workspaces_read_autopilot_objective_result, workspaces_read_checkpoint_request, workspaces_read_checkpoint_result, workspaces_read_file_request, workspaces_read_file_result, workspaces_remove_path_request, workspaces_rename_path_request, workspaces_save_large_paste_request, workspaces_save_large_paste_result, workspaces_stat_file_request, workspaces_stat_file_result, workspaces_truncate_summaries_request, workspace_summary_host_type, workspaces_update_metadata_request, workspaces_workspace_details_host_type, workspaces_write_autopilot_objective_request, workspaces_write_autopilot_objective_result, connector_session_account_result, session_auth_info_result, session_context_attribution, session_context_info, subagent_settings, task_progress, workspace_summary) def to_dict(self) -> dict: result: dict = {} @@ -50489,6 +50682,7 @@ def to_dict(self) -> dict: result["AbortResult"] = to_class(AbortResult, self.abort_result) result["AcceptedEnqueueCommandResult"] = to_class(AcceptedEnqueueCommandResult, self.accepted_enqueue_command_result) result["AccountAllUsers"] = to_class(AccountAllUsers, self.account_all_users) + result["AccountAuthInfo"] = to_class(AccountAuthInfo, self.account_auth_info) result["AccountGetAllUsersResult"] = from_list(lambda x: to_class(AccountAllUsers, x), self.account_get_all_users_result) result["AccountGetCurrentAuthResult"] = to_class(AccountGetCurrentAuthResult, self.account_get_current_auth_result) result["AccountGetQuotaRequest"] = to_class(AccountGetQuotaRequest, self.account_get_quota_request) @@ -50541,6 +50735,7 @@ def to_dict(self) -> dict: result["AuthIdentityMetadata"] = to_class(AuthIdentityMetadata, self.auth_identity_metadata) result["AuthInfo"] = (self.auth_info).to_dict() result["AuthInfoType"] = to_enum(AuthInfoType, self.auth_info_type) + result["AuthLoginAccount"] = to_class(AuthLoginAccount, self.auth_login_account) result["AuthLoginAdvanceRequest"] = to_class(AuthLoginAdvanceRequest, self.auth_login_advance_request) result["AuthLoginBeginRequest"] = to_class(AuthLoginBeginRequest, self.auth_login_begin_request) result["AuthLoginBegun"] = to_class(AuthLoginBegun, self.auth_login_begun) @@ -50987,6 +51182,9 @@ def to_dict(self) -> dict: result["McpConfigList"] = to_class(MCPConfigList, self.mcp_config_list) result["McpConfigRemoveRequest"] = to_class(MCPConfigRemoveRequest, self.mcp_config_remove_request) result["McpConfigUpdateRequest"] = to_class(MCPConfigUpdateRequest, self.mcp_config_update_request) + result["McpConfiguredServer"] = to_class(MCPConfiguredServer, self.mcp_configured_server) + result["McpConfiguredServerList"] = to_class(MCPConfiguredServerList, self.mcp_configured_server_list) + result["McpConfiguredServerState"] = to_class(MCPConfiguredServerState, self.mcp_configured_server_state) result["McpConfigureGitHubRequest"] = to_class(MCPConfigureGitHubRequest, self.mcp_configure_git_hub_request) result["McpConfigureGitHubResult"] = to_class(MCPConfigureGitHubResult, self.mcp_configure_git_hub_result) result["McpDiagnosticDetails"] = to_class(MCPDiagnosticDetails, self.mcp_diagnostic_details) @@ -52034,7 +52232,7 @@ def _load_AgentRegistrySpawnResult(obj: Any) -> "AgentRegistrySpawnResult": case _: raise ValueError(f"Unknown AgentRegistrySpawnResult kind: {kind!r}") # Authentication credentials accepted only at native protocol ingress. Runtime outputs use credential-free `AuthIdentity` metadata. -AuthInfo = HMACAuthInfo | EnvAuthInfo | TokenAuthInfo | TokenProviderAuthInfo | CopilotAPITokenAuthInfo | UserAuthInfo | GhCLIAuthInfo | APIKeyAuthInfo +AuthInfo = HMACAuthInfo | EnvAuthInfo | TokenAuthInfo | TokenProviderAuthInfo | CopilotAPITokenAuthInfo | UserAuthInfo | AccountAuthInfo | GhCLIAuthInfo | APIKeyAuthInfo def _load_AuthInfo(obj: Any) -> "AuthInfo": assert isinstance(obj, dict) @@ -52046,6 +52244,7 @@ def _load_AuthInfo(obj: Any) -> "AuthInfo": case "token-provider": return TokenProviderAuthInfo.from_dict(obj) case "copilot-api-token": return CopilotAPITokenAuthInfo.from_dict(obj) case "user": return UserAuthInfo.from_dict(obj) + case "account": return AccountAuthInfo.from_dict(obj) case "gh-cli": return GhCLIAuthInfo.from_dict(obj) case "api-key": return APIKeyAuthInfo.from_dict(obj) case _: raise ValueError(f"Unknown AuthInfo type: {kind!r}") @@ -52372,7 +52571,7 @@ def _load_SessionOpenParams(obj: Any) -> "SessionOpenParams": case _: raise ValueError(f"Unknown SessionOpenParams kind: {kind!r}") # Authentication credentials accepted by session.gitHubAuth.setCredentials. Session-owned token-provider identities cannot be installed through this method. -SettableAuthInfo = HMACAuthInfo | EnvAuthInfo | SettableTokenAuthInfo | CopilotAPITokenAuthInfo | UserAuthInfo | GhCLIAuthInfo | APIKeyAuthInfo +SettableAuthInfo = HMACAuthInfo | EnvAuthInfo | SettableTokenAuthInfo | CopilotAPITokenAuthInfo | UserAuthInfo | AccountAuthInfo | GhCLIAuthInfo | APIKeyAuthInfo def _load_SettableAuthInfo(obj: Any) -> "SettableAuthInfo": assert isinstance(obj, dict) @@ -52383,6 +52582,7 @@ def _load_SettableAuthInfo(obj: Any) -> "SettableAuthInfo": case "token": return SettableTokenAuthInfo.from_dict(obj) case "copilot-api-token": return CopilotAPITokenAuthInfo.from_dict(obj) case "user": return UserAuthInfo.from_dict(obj) + case "account": return AccountAuthInfo.from_dict(obj) case "gh-cli": return GhCLIAuthInfo.from_dict(obj) case "api-key": return APIKeyAuthInfo.from_dict(obj) case _: raise ValueError(f"Unknown SettableAuthInfo type: {kind!r}") @@ -54276,9 +54476,13 @@ def __init__(self, client: "JsonRpcClient", session_id: str): self.prompts = McpPromptsApi(client, session_id) async def list(self, *, timeout: float | None = None) -> MCPServerList: - "Lists MCP servers configured for the session, their connection status, and host-level state. The host-level state (disabled/filtered servers, failed/needs-auth/pending connections, mcp3p policy, full config) is empty/zero when no MCP host has been initialized for the session.\n\nReturns:\n MCP servers configured for the session, with their connection status and host-level state." + "Lists materialized MCP servers and their connection status. Cache misses may start and wait for MCP servers.\n\nReturns:\n MCP servers configured for the session, with their connection status and host-level state." return MCPServerList.from_dict(await self._client.request("session.mcp.list", {"sessionId": self._session_id}, **_timeout_kwargs(timeout))) + async def list_configured(self, *, timeout: float | None = None) -> MCPConfiguredServerList: + "Lists effective MCP configuration without starting, restarting, authenticating, or waiting for servers. An optional live observation is from an already materialized matching server; this is not a readiness guarantee.\n\nReturns:\n Effective MCP configuration with optional live observations from matching already materialized servers." + return MCPConfiguredServerList.from_dict(await self._client.request("session.mcp.listConfigured", {"sessionId": self._session_id}, **_timeout_kwargs(timeout))) + async def list_tools(self, params: MCPListToolsRequest, *, timeout: float | None = None) -> MCPListToolsResult: "Lists the tools exposed by a connected MCP server on this session's host. This performs a live `tools/list` request. Tool UI metadata is returned independently of whether MCP Apps rendering is enabled for the session.\n\nArgs:\n params: Server name whose tool list should be returned.\n\nReturns:\n Tools exposed by the connected MCP server. Throws when the server is not connected." params_dict: dict[str, Any] = {k: v for k, v in params.to_dict().items() if v is not None} @@ -56145,6 +56349,8 @@ async def handle_installations_confirm(params: dict) -> dict | None: "AbortResult", "AcceptedEnqueueCommandResult", "AccountAllUsers", + "AccountAuthInfo", + "AccountAuthInfoType", "AccountGetAllUsersResult", "AccountGetCurrentAuthResult", "AccountGetQuotaRequest", @@ -56208,6 +56414,7 @@ async def handle_installations_confirm(params: dict) -> dict | None: "AuthIdentityMetadata", "AuthInfo", "AuthInfoType", + "AuthLoginAccount", "AuthLoginAdvanceRequest", "AuthLoginBeginRequest", "AuthLoginBegun", @@ -56710,6 +56917,9 @@ async def handle_installations_confirm(params: dict) -> dict | None: "MCPConfigUpdateRequest", "MCPConfigureGitHubRequest", "MCPConfigureGitHubResult", + "MCPConfiguredServer", + "MCPConfiguredServerList", + "MCPConfiguredServerState", "MCPDiagnosticDetails", "MCPDiagnosticDirection", "MCPDiagnosticKind", diff --git a/python/copilot/generated/session_events.py b/python/copilot/generated/session_events.py index bec6e669ef..502455bedf 100644 --- a/python/copilot/generated/session_events.py +++ b/python/copilot/generated/session_events.py @@ -14260,7 +14260,7 @@ class McpServerStatus(Enum): PENDING = "pending" # The server is configured but disabled. DISABLED = "disabled" - # The server was intentionally stopped and can be restarted on demand when policy permits; a server quarantined by restrictive managed policy stays stopped and cannot be restarted until the policy allows it. + # The server is not running: it may not have started yet, may have been explicitly stopped, or may be quarantined by restrictive managed policy. It can be restarted on demand when policy permits. STOPPED = "stopped" # The server is not configured for this session. NOT_CONFIGURED = "not_configured" diff --git a/rust/src/generated/api_types.rs b/rust/src/generated/api_types.rs index 3532793c48..2f92c44ecd 100644 --- a/rust/src/generated/api_types.rs +++ b/rust/src/generated/api_types.rs @@ -541,6 +541,8 @@ pub mod rpc_methods { pub const SESSION_SKILLS_ENSURELOADED: &str = "session.skills.ensureLoaded"; /// `session.mcp.list` pub const SESSION_MCP_LIST: &str = "session.mcp.list"; + /// `session.mcp.listConfigured` + pub const SESSION_MCP_LISTCONFIGURED: &str = "session.mcp.listConfigured"; /// `session.mcp.listTools` pub const SESSION_MCP_LISTTOOLS: &str = "session.mcp.listTools"; /// `session.mcp.enable` @@ -1610,6 +1612,25 @@ pub struct UserAuthInfo { pub r#type: UserAuthInfoType, } +/// An interactive account whose model provider owns its credentials. It carries no GitHub credential. +/// +///

+/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
+#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct AccountAuthInfo { + /// Host coordinate owned by the account's model provider. + pub host: String, + /// Login identifying the provider-owned account. + pub login: String, + /// Provider-owned account authentication. + pub r#type: AccountAuthInfoType, +} + /// Authentication-info input variant for GitHub CLI credentials, carrying host, login, and the `gh auth token` value. /// ///
@@ -3008,6 +3029,27 @@ pub struct AuthIdentityMetadata { pub r#type: AuthInfoType, } +/// A credential-free account choice after sign-in. +/// +///
+/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
+#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct AuthLoginAccount { + /// Host coordinate owned by the selected account's provider. + pub host: String, + /// Provider kind that owns this account choice. + pub kind: AccountKind, + /// Human-readable login for the account choice. + pub login: String, + /// Opaque identifier supplied to the next login step to select this account. + pub selection_id: String, +} + /// Advance an in-flight login flow, optionally fulfilling an input-required step. /// ///
@@ -3101,7 +3143,7 @@ pub struct AuthLoginStepNeedsInteraction { pub kind: AuthLoginStepNeedsInteractionKind, } -/// Terminal result of an interactive login flow. +/// Result of an interactive login flow. Pending consent or account selection is not terminal. /// ///
/// @@ -3112,13 +3154,16 @@ pub struct AuthLoginStepNeedsInteraction { #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct AuthLoginResultDto { + /// Available accounts when sign-in is awaiting account selection, ordered with Microsoft 365 first. + #[serde(skip_serializing_if = "Option::is_none")] + pub accounts: Option>, /// Host that was signed in, when completed. #[serde(skip_serializing_if = "Option::is_none")] pub host: Option, /// Login that was signed in, when completed. #[serde(skip_serializing_if = "Option::is_none")] pub login: Option, - /// Terminal disposition of the login. + /// Current disposition of the login, including pending user decisions. pub status: AuthLoginResultStatus, } @@ -3134,7 +3179,7 @@ pub struct AuthLoginResultDto { pub struct AuthLoginStepCompleted { /// Login flow step variant discriminator. pub kind: AuthLoginStepCompletedKind, - /// The terminal login result. + /// Login result. When status is needs-plaintext-consent or needs-account-selection, advance with the user's decision to continue. pub result: AuthLoginResultDto, } @@ -11006,6 +11051,71 @@ pub struct McpConfigUpdateRequest { pub name: String, } +/// Observational state for a matching already materialized MCP server. +/// +///
+/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
+#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct McpConfiguredServerState { + /// Observed connection error, when the materialized server failed. + #[serde(skip_serializing_if = "Option::is_none")] + pub error: Option, + /// Observed connection status. This is not a configuration or readiness guarantee. + pub status: McpServerStatus, +} + +/// Effective MCP configuration entry. Configuration enablement is distinct from the optional live observation. +/// +///
+/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
+#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct McpConfiguredServer { + /// Human-readable display name supplied by configuration. + #[serde(skip_serializing_if = "Option::is_none")] + pub display_name: Option, + /// Whether this configured server is enabled after session configuration and policy filtering. + pub enabled: bool, + /// Observed state from an already materialized matching server. Omitted when no live graph has this configured server; it never determines configuration enablement. + #[serde(skip_serializing_if = "Option::is_none")] + pub live: Option, + /// Server name (config key) + pub name: String, + /// Configuration provenance: user, workspace, plugin, builtin, or managed. + #[serde(skip_serializing_if = "Option::is_none")] + pub source: Option, + /// Plugin name that provided this server, when source is plugin. + #[serde(skip_serializing_if = "Option::is_none")] + pub source_plugin: Option, + /// Plugin version that provided this server, when source is plugin. + #[serde(skip_serializing_if = "Option::is_none")] + pub source_plugin_version: Option, +} + +/// Effective MCP configuration with optional live observations from matching already materialized servers. +/// +///
+/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
+#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct McpConfiguredServerList { + /// Effective configured MCP servers. + pub servers: Vec, +} + /// Credential-free authentication identity used to configure GitHub MCP. /// ///
@@ -32167,6 +32277,36 @@ pub struct SessionMcpListResult { pub servers: Vec, } +/// Identifies the target session. +/// +///
+/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
+#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SessionMcpListConfiguredParams { + /// Target session identifier + pub session_id: SessionId, +} + +/// Effective MCP configuration with optional live observations from matching already materialized servers. +/// +///
+/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
+#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SessionMcpListConfiguredResult { + /// Effective configured MCP servers. + pub servers: Vec, +} + /// Tools exposed by the connected MCP server. Throws when the server is not connected. /// ///
@@ -36138,6 +36278,14 @@ pub enum UserAuthInfoType { User, } +/// Provider-owned account authentication. +#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] +pub enum AccountAuthInfoType { + #[serde(rename = "account")] + #[default] + Account, +} + /// Authentication via the `gh` CLI's saved credentials. #[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] pub enum GhCliAuthInfoType { @@ -36171,6 +36319,7 @@ pub enum AuthInfo { TokenProvider(TokenProviderAuthInfo), CopilotApiToken(CopilotApiTokenAuthInfo), User(UserAuthInfo), + Account(AccountAuthInfo), GhCli(GhCliAuthInfo), ApiKey(ApiKeyAuthInfo), } @@ -36877,6 +37026,9 @@ pub enum AuthInfoType { /// Authentication from an interactive user sign-in. #[serde(rename = "user")] User, + /// Authentication from a selected provider-owned account, without a GitHub credential. + #[serde(rename = "account")] + Account, /// Authentication delegated to the GitHub CLI. #[serde(rename = "gh-cli")] GhCli, @@ -36938,7 +37090,7 @@ pub enum AuthLoginStepCompletedKind { Completed, } -/// Terminal disposition of a login persistence attempt. +/// Disposition of a login attempt, including pending user decisions. /// ///
/// @@ -36948,12 +37100,15 @@ pub enum AuthLoginStepCompletedKind { ///
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] pub enum AuthLoginResultStatus { - /// The credential was persisted and the account is signed in. + /// The credential was persisted and the selected account is signed in. #[serde(rename = "completed")] Completed, /// Persistence needs explicit consent to store the token in plaintext. #[serde(rename = "needs-plaintext-consent")] NeedsPlaintextConsent, + /// Credentials are saved; select an account using a returned selectionId as advance input to complete sign-in. + #[serde(rename = "needs-account-selection")] + NeedsAccountSelection, /// The user declined plaintext persistence. #[serde(rename = "declined")] Declined, @@ -44146,6 +44301,7 @@ pub enum SettableAuthInfo { Token(SettableTokenAuthInfo), CopilotApiToken(CopilotApiTokenAuthInfo), User(UserAuthInfo), + Account(AccountAuthInfo), GhCli(GhCliAuthInfo), ApiKey(ApiKeyAuthInfo), } diff --git a/rust/src/generated/rpc.rs b/rust/src/generated/rpc.rs index cf5da5b4a5..d14723e670 100644 --- a/rust/src/generated/rpc.rs +++ b/rust/src/generated/rpc.rs @@ -7846,7 +7846,7 @@ impl<'a> SessionRpcMcp<'a> { } } - /// Lists MCP servers configured for the session, their connection status, and host-level state. The host-level state (disabled/filtered servers, failed/needs-auth/pending connections, mcp3p policy, full config) is empty/zero when no MCP host has been initialized for the session. + /// Lists materialized MCP servers and their connection status. Cache misses may start and wait for MCP servers. /// /// Wire method: `session.mcp.list`. /// @@ -7871,6 +7871,31 @@ impl<'a> SessionRpcMcp<'a> { Ok(serde_json::from_value(_value)?) } + /// Lists effective MCP configuration without starting, restarting, authenticating, or waiting for servers. An optional live observation is from an already materialized matching server; this is not a readiness guarantee. + /// + /// Wire method: `session.mcp.listConfigured`. + /// + /// # Returns + /// + /// Effective MCP configuration with optional live observations from matching already materialized servers. + /// + ///
+ /// + /// **Experimental.** This API is part of an experimental wire-protocol surface + /// and may change or be removed in future SDK or CLI releases. Pin both the + /// SDK and CLI versions if your code depends on it. + /// + ///
+ pub async fn list_configured(&self) -> Result { + let wire_params = serde_json::json!({ "sessionId": self.session.id() }); + let _value = self + .session + .client() + .call(rpc_methods::SESSION_MCP_LISTCONFIGURED, Some(wire_params)) + .await?; + Ok(serde_json::from_value(_value)?) + } + /// Lists the tools exposed by a connected MCP server on this session's host. This performs a live `tools/list` request. Tool UI metadata is returned independently of whether MCP Apps rendering is enabled for the session. /// /// Wire method: `session.mcp.listTools`. diff --git a/rust/src/generated/session_events.rs b/rust/src/generated/session_events.rs index 569a16a92c..1e9dff8586 100644 --- a/rust/src/generated/session_events.rs +++ b/rust/src/generated/session_events.rs @@ -11553,7 +11553,7 @@ pub enum McpServerStatus { /// The server is configured but disabled. #[serde(rename = "disabled")] Disabled, - /// The server was intentionally stopped and can be restarted on demand when policy permits; a server quarantined by restrictive managed policy stays stopped and cannot be restarted until the policy allows it. + /// The server is not running: it may not have started yet, may have been explicitly stopped, or may be quarantined by restrictive managed policy. It can be restarted on demand when policy permits. #[serde(rename = "stopped")] Stopped, /// The server is not configured for this session. diff --git a/rust/tests/api_types_test.rs b/rust/tests/api_types_test.rs index fa714a7789..d0b670f0af 100644 --- a/rust/tests/api_types_test.rs +++ b/rust/tests/api_types_test.rs @@ -11,11 +11,13 @@ use github_copilot_sdk::rpc::{ CustomizationReloadOutcome, CustomizationReloadStatus, CustomizationReloadSubsystem, EnqueueCommandResult, Extension, ExtensionList, ExtensionSource, ExtensionStatus, ExtensionsDisableRequest, ExtensionsEnableRequest, FleetStartRequest, FleetStartResult, - McpDisableRequest, McpEnableOptions, McpEnableRequest, McpInstallationOperationStatus, - McpOauthLoginOptions, McpOauthLoginRequest, McpServer, McpStopServerRequest, - ModelSetAllowedModelsRequest, ModelSetAllowedModelsResult, ModelSwitchAutoTierRequest, - ModelSwitchAutoTierResult, ModelSwitchAutoTierStatus, QueuePendingItems, QueuePendingItemsKind, - SandboxConfig, SendAgentMode, TasksStartAgentRequest, UnsupportedEnqueueCommandResult, + McpConfiguredServer, McpConfiguredServerList, McpConfiguredServerState, McpDisableRequest, + McpEnableOptions, McpEnableRequest, McpInstallationOperationStatus, McpOauthLoginOptions, + McpOauthLoginRequest, McpServer, McpStopServerRequest, ModelSetAllowedModelsRequest, + ModelSetAllowedModelsResult, ModelSwitchAutoTierRequest, ModelSwitchAutoTierResult, + ModelSwitchAutoTierStatus, QueuePendingItems, QueuePendingItemsKind, SandboxConfig, + SendAgentMode, SessionMcpListConfiguredResult, TasksStartAgentRequest, + UnsupportedEnqueueCommandResult, }; use github_copilot_sdk::session_events::{ McpServerStatus, PermissionRequest, PermissionRequestedData, SessionEventData, @@ -23,6 +25,62 @@ use github_copilot_sdk::session_events::{ }; use github_copilot_sdk::{AutoTier, AutoTierPreference, SetModelOptions}; +#[test] +fn configured_mcp_servers_preserve_enablement_without_live_state() { + for enabled in [false, true] { + let server = McpConfiguredServer { + name: "configured".to_string(), + enabled, + ..Default::default() + }; + let wire = serde_json::json!({ + "servers": [{ "name": "configured", "enabled": enabled }], + }); + assert_eq!( + serde_json::to_value(McpConfiguredServerList { + servers: vec![server], + }) + .unwrap(), + wire + ); + let result: SessionMcpListConfiguredResult = serde_json::from_value(wire.clone()).unwrap(); + assert!(result.servers[0].live.is_none()); + assert_eq!(serde_json::to_value(result).unwrap(), wire); + } +} + +#[test] +fn configured_mcp_servers_preserve_live_observations_independently_of_enablement() { + for enabled in [false, true] { + let server = McpConfiguredServer { + name: "configured".to_string(), + enabled, + live: Some(McpConfiguredServerState { + status: McpServerStatus::Stopped, + error: None, + }), + ..Default::default() + }; + assert_eq!( + serde_json::to_value(server).unwrap(), + serde_json::json!({ + "name": "configured", + "enabled": enabled, + "live": { "status": "stopped" }, + }) + ); + } + let wire = serde_json::json!({ + "servers": [{ + "name": "configured", + "enabled": true, + "live": { "status": "failed", "error": "connection failed" }, + }], + }); + let result: SessionMcpListConfiguredResult = serde_json::from_value(wire.clone()).unwrap(); + assert_eq!(serde_json::to_value(result).unwrap(), wire); +} + #[test] fn customization_reload_outcome_preserves_future_wire_values() { let wire = serde_json::json!({ diff --git a/scripts/codegen/python.ts b/scripts/codegen/python.ts index 3ca936f461..787fe2ca6e 100644 --- a/scripts/codegen/python.ts +++ b/scripts/codegen/python.ts @@ -308,10 +308,27 @@ export function pythonAppendLastFieldsPresentIn( definitions: Record, fields: ReadonlyArray = PY_RPC_APPEND_LAST_FIELDS ): ReadonlyArray { - return fields.filter(([className, propertyName]) => { + const result = fields.filter(([className, propertyName]) => { const definition = definitions[className]; return typeof definition === "object" && Object.hasOwn(definition.properties ?? {}, propertyName); }); + const seen = new Set(result.map(([className, propertyName]) => `${className}.${propertyName}`)); + + for (const [className, definition] of Object.entries(definitions)) { + if (typeof definition !== "object") continue; + for (const [propertyName, property] of Object.entries(definition.properties ?? {})) { + if ( + typeof property !== "object" || + property["x-copilot-sdk-append-last"] !== true || + seen.has(`${className}.${propertyName}`) + ) { + continue; + } + result.push([className, propertyName]); + } + } + + return result; } function preservePythonRpcStringDateFields(definitions: Record): void { diff --git a/test/harness/stdio-shutdown-runtime.cjs b/test/harness/stdio-shutdown-runtime.cjs index 99d1c8cec2..fe7dc1cc09 100644 --- a/test/harness/stdio-shutdown-runtime.cjs +++ b/test/harness/stdio-shutdown-runtime.cjs @@ -4,7 +4,7 @@ // Shared SDK shutdown fixture: node