Rust SDK API review fixes#1367
Conversation
- Rename Session::get_messages() to get_events(); keep get_messages as a #[deprecated] alias for one release. - Rename ResumeSessionConfig::disable_resume to suppress_resume_event. Wire field stays disableResume for runtime compatibility. - Add #[deprecated] to Session::destroy(); point callers at disconnect(). - Update examples (chat, hooks, lifecycle_observer, tool_server) and all unit/integration test callsites. - Add tests: serde round-trip for suppress_resume_event <-> disableResume, and an explicit #[allow(deprecated)] test exercising the get_messages alias. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
McpStdioServerConfig.tools and McpHttpServerConfig.tools change from Vec<String> to Option<Vec<String>> with #[serde(default, skip_serializing_if = Option::is_none)]. Semantics now match the runtime contract and the other SDKs: - None (field omitted on wire) -> expose ALL tools - Some(vec![]) -> expose NO tools - Some(non-empty) -> explicit allowlist Previously an empty Vec was treated as 'no tools' on the wire but the runtime treats omission as 'all tools', so there was no way to opt back into 'all' once the field was set. Adds four tri-state serde tests (serialize + deserialize for each of stdio and http). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
When ClientOptions::log_level is None, the SDK no longer passes --log-level to the spawned CLI process at all. The CLI's built-in default takes over. Previously the SDK silently overrode the CLI default with Info, which made it impossible to opt back into the CLI default. - Extract a log_level_args() helper alongside auth_args / remote_args. - Update field rustdoc and remove 'Default.' annotation from LogLevel::Info. - Add two unit tests: log_level_args_omitted_when_unset and log_level_args_emit_flag_when_set. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Renames the public UI-input options struct to match the Go SDK and to make the connection to SessionUi::input explicit. Touches three files (src/types.rs, src/session.rs, tests/e2e/elicitation.rs); zero behaviour change. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Folds previously-scattered transport configuration into the Transport enum:
Transport::Tcp { port, connection_token }
Transport::External { host, port, connection_token }
Removes:
- ClientOptions::tcp_connection_token + with_tcp_connection_token
- the runtime check that rejected (Stdio + token) -- now a type error
- public ConnectionState enum and Client::state() accessor
Renames:
- ClientOptions::copilot_home -> base_directory + with_base_directory
- ClientOptions::remote -> enable_remote_sessions
+ with_enable_remote_sessions
- the runtime env var passed to the child stays COPILOT_HOME
- the spawn arg stays --remote
ConnectionState is demoted to pub(crate) and loses its serde derives;
nothing outside the crate reads it. The internal state-tracking field
on ClientInner is unchanged.
All call sites in tests, examples, and e2e support helpers updated to
the new Transport shape. Adds a unit test for empty-string
connection_token on External transport (the existing Tcp test was
adapted, the External one is new).
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Phase H -- match TS/C# semantically for permission/elicitation/external-tool
broadcasts on shared CLI connections:
1. Three new SessionHandler probe methods (default false):
wants_permission_dispatch()
wants_elicitation_dispatch()
wants_external_tool_dispatch(tool_name)
ApproveAllHandler and DenyAllHandler override to true for permission;
NoopHandler keeps the safe default of false everywhere.
PermissionOverrideHandler claims permission and forwards the others
to its inner handler. ToolHandlerRouter advertises the specific tool
names it knows.
2. session.rs broadcast dispatcher gates on the probes BEFORE invoking
the handler or sending an RPC response:
- permission.requested: skip if data.resolvedByHook is true, skip
if !wants_permission_dispatch()
- elicitation.requested: skip if !wants_elicitation_dispatch()
- external_tool.requested: skip if !wants_external_tool_dispatch(name)
3. SessionConfig::default() / ResumeSessionConfig::new() no longer
hardcode requestPermission=true / requestElicitation=true. Those
wire flags are now derived from the handler probes at
Client::create_session and Client::resume_session time, so the
runtime never broadcasts permission/elicitation events to a client
that wouldn't respond.
4. Removed the public SessionConfig::with_request_permission /
with_request_elicitation builders (and ResumeSessionConfig
equivalents) -- the flags are derived now, not user-set.
Phase I -- approve_all_permissions ordering trap:
The previous design wrapped the handler in-place inside the policy
builder, which made the call order to with_handler significant. Now
permission_policy lives in a separate (pub(crate)) field on
SessionConfig/ResumeSessionConfig and the wrap is applied at
Client::create_session / Client::resume_session time, after both fields
are finalised. Call order is now irrelevant.
Tests:
- request_elicitation_sent_in_create_params updated for the new
derivation (ApproveAllHandler -> requestPermission=true,
requestElicitation=false).
- new noop_handler_sends_request_permission_false validates the
H1a wire-flag derivation in both directions.
- new external_tool_broadcast_for_unknown_tool_is_not_responded_to,
permission_broadcast_with_resolved_by_hook_is_not_responded_to,
permission_broadcast_with_no_claiming_handler_is_not_responded_to,
elicitation_broadcast_with_no_claiming_handler_is_not_responded_to.
- new session_config_*_is_order_independent tests for Phase I.
- multi_client e2e tests' PermissionDecisionHandler /
SelectiveToolHandler updated to advertise the probes.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- rust/README.md: drop the 'call approve_all_permissions AFTER with_handler' warning -- the policy field makes both orderings equivalent now. Replace the elicitation snippet with the new wants_elicitation_dispatch probe. - docs/features/remote-sessions.md: with_remote -> with_enable_remote_sessions. - test/scenarios/**/rust: rename .destroy() -> .disconnect(). - test/scenarios/transport/tcp/rust: add connection_token field to Transport::External literal. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…create TS, C#, Go, and Python all currently hardcoded requestPermission: true on session.create regardless of whether the caller supplied an onPermissionRequest handler. (Resume / join paths already derived from handler presence.) The runtime supports the presence-derived shape: when no client opts in via requestPermission=true, the session short-circuits permission prompts with user-not-available instead of broadcasting. So the hardcoded true forced the runtime to broadcast permission events to clients that would never respond, wasting a roundtrip and creating a discrepancy between create and resume. Aligns all four SDKs to: requestPermission := onPermissionRequest != null on both session.create and session.resume. (Rust will land the same shape in its API-review-fixes commit on this branch.) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This comment has been minimized.
This comment has been minimized.
Replaces the monolithic SessionHandler trait with five focused
single-method optional handler traits in src/handler.rs:
PermissionHandler, ElicitationHandler, UserInputHandler,
ExitPlanModeHandler, AutoModeSwitchHandler. Each handler is an optional
field on SessionConfig / ResumeSessionConfig; presence is the signal,
matching TypeScript/C# semantics.
- Drops SessionHandler, HandlerEvent, HandlerResponse, NoopHandler,
ToolHandlerRouter, and the wants_*_dispatch probe methods.
- ApproveAllHandler / DenyAllHandler now implement PermissionHandler.
- Tool dispatch lives in an internal HashMap<name, Arc<dyn ToolHandler>>
built from SessionConfig::tool_handlers. ToolHandlerRouter is gone;
callers pass handlers directly via with_tool_handlers(...).
- Wire flags on session.create / session.resume (requestPermission,
requestElicitation, requestUserInput, requestExitPlanMode,
requestAutoModeSwitch, hooks) are derived from handler presence at
Client::create_session / resume_session time. No probes, no
caller-visible knobs.
- New src/wire.rs: SessionCreateWire and SessionResumeWire structs
(pub(crate)) carry the wire payload. SessionConfig::to_wire() and
ResumeSessionConfig::to_wire() build them. SessionConfig and
ResumeSessionConfig keep their serde derives (used only for tests),
but Client::create_session and Client::resume_session now serialize
the wire struct, not the user-facing config.
- permission::approve_all / deny_all / approve_if are now
Arc<dyn PermissionHandler> producers. New private
permission::resolve_handler(handler, policy) used by Client to apply
a stored policy on top of (or in lieu of) a caller-supplied handler --
the policy wins, making with_permission_handler / approve_all_permissions
order-independent.
- Broadcast dispatcher in session.rs:
- permission.requested: skip if data.resolvedByHook OR
permission_handler is None; else dispatch via handle().
- elicitation.requested: skip if elicitation_handler is None.
- external_tool.requested: look up by tool_name in the registry;
silently skip if absent.
- Direct RPC dispatcher in session.rs:
- tool.call: REMOVED (legacy; runtime only emits external_tool.requested
broadcasts now).
- permission.request: kept as v2 back-compat; returns
{ kind: 'user-not-available' } when no permission handler.
- userInput.request / exitPlanMode.request / autoModeSwitch.request:
kept (this is how the runtime still emits these). Defaults when
no handler match TS: { noResponse: true }, { approved: true },
and { response: 'no' }.
Tests in rust/tests/ and e2e tests are migrated in a follow-up commit.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This comment has been minimized.
This comment has been minimized.
…dler API Mechanical migration of all consumers to the new optional handler-trait API introduced in 455a17b: - rust/tests/session_test.rs: ~30 SessionHandler impls split into the corresponding PermissionHandler / ElicitationHandler / UserInputHandler / ExitPlanModeHandler / AutoModeSwitchHandler / ToolHandler impls. Tests that called handler.on_event(...) directly now call handler.handle(...) on the appropriate trait. The legacy tool.call direct-RPC test was dropped; equivalent coverage is already provided by external_tool_requested_dispatches_to_handler_and_responds. - rust/tests/e2e/*: per-trait migration; with_handler(...) -> the matching with_*_handler builder. - rust/examples/*: same migration. - test/scenarios/**/rust/src/main.rs: same migration. Also fixes a bug in Client::create_session / Client::resume_session where to_wire() was called after .take()-ing all handler fields, so the derived wire flags (requestPermission, requestElicitation, hooks, etc.) were always false. Computing the wire payload now happens before the takes. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…handler types Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
… in session.rs Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This comment has been minimized.
This comment has been minimized.
The request_user_input / request_permission / request_elicitation / request_exit_plan_mode / request_auto_mode_switch fields on SessionConfig and ResumeSessionConfig were unused: to_wire() derives each flag from handler presence, matching the per-trait dispatch semantics of the TypeScript and C# SDKs. Leaving the public fields (and their with_request_* builders) in place would have been a footgun: callers could set them and silently get no effect. Removed: - pub request_user_input / request_permission / request_elicitation / request_exit_plan_mode / request_auto_mode_switch fields from SessionConfig and ResumeSessionConfig. - with_request_user_input / with_request_exit_plan_mode / with_request_auto_mode_switch builders on both configs. - The corresponding entries in Default impls and Debug impls. - Vestigial assertions in the in-file builder tests. The two 'default flags' tests are replaced with new tests that build the wire payload directly and assert every request_* flag (and hooks) is false when no handler is installed. The existing 'request_elicitation_sent_in_create_params' and 'noop_handler_sends_request_permission_false' integration tests already assert via the wire serialization, so they are unchanged. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This comment has been minimized.
This comment has been minimized.
Phase B made McpStdioServerConfig.tools an Option<Vec<String>>; the scenario still set tools: vec![*], which under the new tri-state contract would mean 'only allow the tool literally named *', not 'all tools'. Drop the field and rely on Default (None) to expose the full MCP server tool list. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This comment has been minimized.
This comment has been minimized.
ElicitationHandler is imported in types.rs, so the bare label resolves to the same item; the explicit path triggers rustdoc::redundant-explicit-links. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This comment has been minimized.
This comment has been minimized.
Earlier PowerShell editing of this file double-corrupted some em-dashes, right-arrows, and a section sign (the bytes had been UTF-8 -> cp1252 -> UTF-8 -> cp1252 -> UTF-8 round-tripped). Restored to plain UTF-8. 19 instances fixed (11 em-dashes, 7 right-arrows, 1 section sign). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This comment has been minimized.
This comment has been minimized.
The request_* wire flags are no longer caller-controllable; they are derived from handler presence. Installing with_user_input_handler is sufficient to set requestUserInput=true on the wire. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Cross-SDK Consistency Review ✅This PR applies the
All SDKs now derive The Java SDK only has a README stub at this point (no client implementation), so it is not affected. No cross-SDK consistency issues identified.
|
There was a problem hiding this comment.
Pull request overview
This PR refactors the Rust SDK’s public callback/handler surface to match the cross-SDK API review direction (focused per-event handler traits, wire payload structs separated from user config), and updates scenarios/tests/examples accordingly. It also applies a companion cross-SDK fix so requestPermission is derived from handler presence (enabling correct multi-client behavior).
Changes:
- Rust: Replace the legacy
SessionHandlermega-trait andToolHandlerRouterpattern with focused handler traits andSessionConfig::with_*_handler/with_tool_handlers, plus a dedicated wire payload module. - Rust: Rename/adjust key APIs used by tests/scenarios (e.g.,
get_messages→get_events,destroy→disconnect, transport connection token changes). - Cross-SDK: Derive
requestPermissionfrom permission handler presence in Node.js, Python, Go, and .NET clients.
Show a summary per file
| File | Description |
|---|---|
| test/scenarios/transport/tcp/rust/src/main.rs | Updates TCP scenario for new transport token field and new session config/teardown APIs. |
| test/scenarios/transport/stdio/rust/src/main.rs | Updates stdio scenario to new permission handler builder and disconnect(). |
| test/scenarios/tools/tool-overrides/rust/src/main.rs | Migrates tool override scenario to ToolHandler + with_tool_handlers API. |
| test/scenarios/tools/tool-filtering/rust/src/main.rs | Migrates tool filtering scenario to new permission handler builder + disconnect(). |
| test/scenarios/tools/skills/rust/src/main.rs | Migrates skills scenario to new handler builders and disconnect(). |
| test/scenarios/tools/no-tools/rust/src/main.rs | Migrates “no tools” scenario to new permission handler builder + disconnect(). |
| test/scenarios/tools/mcp-servers/rust/src/main.rs | Updates MCP server scenario for new MCP tools tri-state semantics and new handler builder. |
| test/scenarios/tools/custom-agents/rust/src/main.rs | Migrates custom agents scenario to tool handlers + new permission handler builder. |
| test/scenarios/sessions/streaming/rust/src/main.rs | Reworks streaming scenario to use session.subscribe() for deltas and new handler model. |
| test/scenarios/sessions/session-resume/rust/src/main.rs | Migrates session resume scenario to per-trait handlers and disconnect(). |
| test/scenarios/sessions/infinite-sessions/rust/src/main.rs | Migrates infinite sessions scenario to new handler builder + disconnect(). |
| test/scenarios/sessions/concurrent-sessions/rust/src/main.rs | Migrates concurrent sessions scenario to new handler builder + disconnect(). |
| test/scenarios/prompts/system-message/rust/src/main.rs | Migrates prompt scenario to new handler builder + disconnect(). |
| test/scenarios/prompts/reasoning-effort/rust/src/main.rs | Migrates prompt scenario to new handler builder + disconnect(). |
| test/scenarios/prompts/attachments/rust/src/main.rs | Migrates attachments scenario to new handler builder + disconnect(). |
| test/scenarios/modes/default/rust/src/main.rs | Migrates default mode scenario to new handler builder + disconnect(). |
| test/scenarios/callbacks/user-input/rust/src/main.rs | Migrates user input callback scenario to split handler traits. |
| test/scenarios/callbacks/permissions/rust/src/main.rs | Migrates permission callback scenario to PermissionHandler. |
| test/scenarios/callbacks/hooks/rust/src/main.rs | Migrates hooks scenario to new permission handler builder + disconnect(). |
| rust/tests/e2e/tools.rs | Updates tool E2E tests for tool handler registration + per-trait permission handlers. |
| rust/tests/e2e/tool_results.rs | Updates tool result E2E tests for tool handler registration model. |
| rust/tests/e2e/telemetry.rs | Updates telemetry E2E for tool handler registration model. |
| rust/tests/e2e/system_message_transform.rs | Removes/empties the prior system message transform E2E coverage file content. |
| rust/tests/e2e/suspend.rs | Removes/empties the prior suspend E2E coverage file content. |
| rust/tests/e2e/support.rs | Updates E2E helpers for new APIs (get_events, new TCP token placement, new handler builder). |
| rust/tests/e2e/streaming_fidelity.rs | Updates streaming fidelity E2E tests to new handler builder + get_events. |
| rust/tests/e2e/session.rs | Broad migration of session E2E tests to new handler/tool registration APIs and event history rename. |
| rust/tests/e2e/session_lifecycle.rs | Updates lifecycle E2E tests to get_events. |
| rust/tests/e2e/session_fs.rs | Removes/empties the prior session FS E2E coverage file content. |
| rust/tests/e2e/rpc_mcp_and_skills.rs | Updates RPC E2E for MCP tools tri-state and new APIs. |
| rust/tests/e2e/rpc_event_side_effects.rs | Updates RPC side effects E2E to get_events. |
| rust/tests/e2e/permissions.rs | Migrates permissions E2E tests to PermissionHandler trait and new builder. |
| rust/tests/e2e/per_session_auth.rs | Updates per-session auth E2E tests to new permission handler builder. |
| rust/tests/e2e/pending_work_resume.rs | Updates pending-work resume E2E tests for new transport token + handler model. |
| rust/tests/e2e/multi_client.rs | Updates multi-client E2E tests for new handler/tool registration model and resume flag rename. |
| rust/tests/e2e/multi_client_commands_elicitation.rs | Updates multi-client elicitation E2E tests to split handler traits and new transport token. |
| rust/tests/e2e/mode_handlers.rs | Migrates mode handler E2E tests to ExitPlanModeHandler / AutoModeSwitchHandler. |
| rust/tests/e2e/mcp_and_agents.rs | Removes/empties the prior MCP+agents E2E coverage file content. |
| rust/tests/e2e/hooks_extended.rs | Migrates extended hooks E2E tests to new tool handler registration model. |
| rust/tests/e2e/event_fidelity.rs | Updates event fidelity E2E tests to get_events. |
| rust/tests/e2e/error_resilience.rs | Removes/empties the prior error resilience E2E coverage file content. |
| rust/tests/e2e/elicitation.rs | Migrates elicitation E2E tests to ElicitationHandler / PermissionHandler split and renames. |
| rust/tests/e2e/compaction.rs | Removes/empties the prior compaction E2E coverage file content. |
| rust/tests/e2e/commands.rs | Removes/empties the prior commands E2E coverage file content. |
| rust/tests/e2e/client.rs | Updates client E2E tests for new transport token placement and removal of connection state API. |
| rust/tests/e2e/client_options.rs | Removes/empties the prior client options E2E coverage file content. |
| rust/tests/e2e/client_lifecycle.rs | Updates lifecycle E2E tests for removal of connection state API. |
| rust/tests/e2e/ask_user.rs | Migrates ask_user E2E tests to split handler traits. |
| rust/tests/e2e/abort.rs | Migrates abort E2E tests to new tool handler registration model. |
| rust/src/wire.rs | Introduces dedicated wire-format structs for session.create / session.resume payloads. |
| rust/src/tool.rs | Updates tool framework docs and removes ToolHandlerRouter in favor of internal handler registry. |
| rust/src/subscription.rs | Updates subscription documentation to reflect new handler model. |
| rust/src/permission.rs | Refactors permission policy helpers to produce PermissionHandler and adds deterministic resolution. |
| rust/src/lib.rs | Updates transport/auth/remote/log-level config surface and client startup token handling. |
| rust/src/handler.rs | Replaces the mega-trait with focused optional handler traits; updates built-in handlers accordingly. |
| rust/README.md | Updates Rust SDK documentation and examples to the new handler and tool registration model. |
| rust/examples/tool_server.rs | Migrates tool server example to with_tool_handlers + permission handler builder. |
| rust/examples/session_fs.rs | Updates session FS example to new permission handler builder. |
| rust/examples/lifecycle_observer.rs | Updates lifecycle observer example to avoid removed connection state API and use pid(). |
| rust/examples/hooks.rs | Updates hooks example to new permission handler builder and disconnect(). |
| rust/examples/chat.rs | Updates chat example to split handlers and use subscribe() for streamed deltas. |
| python/copilot/client.py | Derives requestPermission from whether a permission handler is provided. |
| nodejs/src/client.ts | Derives requestPermission from onPermissionRequest presence. |
| go/client.go | Sends RequestPermission only when a permission handler is configured. |
| dotnet/src/Client.cs | Omits requestPermission unless a permission handler is configured. |
| docs/features/remote-sessions.md | Updates Rust snippet to with_enable_remote_sessions(true). |
Copilot's findings
Comments suppressed due to low confidence (2)
rust/tests/e2e/rpc_mcp_and_skills.rs:445
McpStdioServerConfig.toolsis now tri-state whereNonemeans "expose all tools". Settingtools: Some(vec!["*"])will be treated as a literal tool name filter and likely exposes no real tools; usetools: None/omit the field to expose all.
rust/tests/e2e/compaction.rs:2- This integration test file is now empty. Consider deleting it entirely (so Cargo doesn’t build a no-op test crate) or adding a brief comment explaining why an empty placeholder file is intentionally kept.
- Files reviewed: 71/71 changed files
- Comments generated: 10
| let mut config = SessionConfig::default(); | ||
| config.model = Some("claude-haiku-4.5".to_string()); | ||
| config.request_user_input = Some(true); | ||
| let config = config | ||
| .with_handler(handler) | ||
| .with_permission_handler(handler.clone()) | ||
| .with_user_input_handler(handler) | ||
| .with_hooks(Arc::new(AllowAllHooks)); | ||
|
|
| use async_trait::async_trait; | ||
| use github_copilot_sdk::handler::{ElicitationHandler, ElicitationResult}; | ||
| use github_copilot_sdk::types::{ElicitationRequestData, RequestId, SessionId}; | ||
|
|
||
| struct MyElicitation; | ||
|
|
||
| #[async_trait] | ||
| impl ElicitationHandler for MyElicitation { | ||
| async fn handle( | ||
| &self, | ||
| _sid: SessionId, | ||
| _rid: RequestId, | ||
| _data: ElicitationRequestData, | ||
| ) -> ElicitationResult { |
| let tool_handlers: Vec<Arc<dyn ToolHandler>> = | ||
| vec![Arc::new(GetWeatherTool), Arc::new(RollDiceTool)]; | ||
| let tools: Vec<Tool> = tool_handlers.iter().map(|h| h.tool()).collect(); | ||
|
|
||
| let client = Client::start(ClientOptions::default()).await?; | ||
|
|
||
| let config = { | ||
| let mut cfg = SessionConfig::default(); | ||
| let mut cfg = SessionConfig::default() | ||
| .with_permission_handler(Arc::new(ApproveAllHandler)) | ||
| .with_tool_handlers(tool_handlers); | ||
| cfg.tools = Some(tools); | ||
| cfg.with_handler(handler) | ||
| cfg |
| let tool_handlers: Vec<Arc<dyn ToolHandler>> = vec![Arc::new(EncryptStringTool)]; | ||
| let tools: Vec<Tool> = tool_handlers.iter().map(|h| h.tool()).collect(); | ||
| let __perm = Arc::new(ApproveAllHandler); | ||
| let session = client | ||
| .create_session( | ||
| SessionConfig::default() | ||
| .with_github_token(super::support::DEFAULT_TEST_TOKEN) | ||
| .with_handler(Arc::new(router)) | ||
| .with_permission_handler(__perm) | ||
| .with_tool_handlers(tool_handlers) | ||
| .with_tools(tools), | ||
| ) |
| let tool_handlers: Vec<Arc<dyn ToolHandler>> = vec![Arc::new(tool)]; | ||
| let tools: Vec<Tool> = tool_handlers.iter().map(|h| h.tool()).collect(); | ||
| let __perm = Arc::new(ApproveAllHandler); | ||
| client | ||
| .create_session( | ||
| SessionConfig::default() | ||
| .with_github_token(super::support::DEFAULT_TEST_TOKEN) | ||
| .with_handler(Arc::new(router)) | ||
| .with_permission_handler(__perm) | ||
| .with_tool_handlers(tool_handlers) | ||
| .with_tools(tools), |
| let tool_handlers: Vec<Arc<dyn ToolHandler>> = vec![Arc::new(EchoTelemetryTool { | ||
| name: tool_name.to_string(), | ||
| })], | ||
| Arc::new(ApproveAllHandler), | ||
| ); | ||
| let tools = router.tools(); | ||
| })]; | ||
| let tools: Vec<Tool> = tool_handlers.iter().map(|h| h.tool()).collect(); | ||
| let __perm = Arc::new(ApproveAllHandler); | ||
| let session = client | ||
| .create_session( | ||
| SessionConfig::default() | ||
| .with_github_token(super::support::DEFAULT_TEST_TOKEN) | ||
| .with_handler(Arc::new(router)) | ||
| .with_permission_handler(__perm) | ||
| .with_tool_handlers(tool_handlers) | ||
| .with_tools(tools), |
| let tool_handlers: Vec<Arc<dyn ToolHandler>> = vec![Arc::new(SecretTool)]; | ||
| let tools: Vec<Tool> = tool_handlers.iter().map(|h| h.tool()).collect(); | ||
| let __perm = Arc::new(ApproveAllHandler); | ||
| let session = client | ||
| .create_session( | ||
| SessionConfig::default() | ||
| .with_github_token(super::support::DEFAULT_TEST_TOKEN) | ||
| .with_handler(Arc::new(router)) | ||
| .with_permission_handler(__perm) | ||
| .with_tool_handlers(tool_handlers) | ||
| .with_tools(tools) | ||
| .with_default_agent(DefaultAgentConfig { |
| let echo_tool: Arc<dyn ToolHandler> = Arc::new(EchoValueTool); | ||
| let tools = vec![echo_tool.tool()]; | ||
| let client = ctx.start_client().await; | ||
| let session = client | ||
| .create_session( | ||
| SessionConfig::default() | ||
| .with_github_token(super::support::DEFAULT_TEST_TOKEN) | ||
| .with_handler(Arc::new(router)) | ||
| .with_permission_handler(Arc::new(ApproveAllHandler)) | ||
| .with_tool_handlers(vec![echo_tool]) | ||
| .with_tools(tools) | ||
| .with_hooks(Arc::new(RecordingHooks::pre_tool(tx))), |
| let mut config = SessionConfig::default(); | ||
| config.model = Some("claude-haiku-4.5".to_string()); | ||
| config.tools = Some(tools); | ||
| config.default_agent = Some(DefaultAgentConfig { | ||
| excluded_tools: Some(vec!["analyze-codebase".to_string()]), | ||
| }); | ||
| config.custom_agents = Some(vec![researcher]); | ||
| let config = config.with_handler(Arc::new(router)); | ||
| let config = config | ||
| .with_permission_handler(Arc::new(ApproveAllHandler)) | ||
| .with_tool_handlers(vec![analyze_codebase]); | ||
|
|
| @@ -42,7 +41,9 @@ async fn main() -> Result<(), github_copilot_sdk::Error> { | |||
| let mut config = SessionConfig::default(); | |||
| config.model = Some("claude-haiku-4.5".to_string()); | |||
| config.tools = Some(tools); | |||
| let config = config.with_handler(Arc::new(router)); | |||
| let config = config | |||
| .with_permission_handler(Arc::new(ApproveAllHandler)) | |||
| .with_tool_handlers(vec![grep_tool]); | |||
|
|
|||
Rust SDK API review fixes
This PR brings the Rust SDK in line with the cross-SDK API review work already merged for C# (#1343), TypeScript (#1357), and Go (#1360), and applies the Rust-specific items from
api_review_rust.md. It is a deliberate breaking-change pass, intentionally landed before we declare the SDK stable.The end result is a tighter API surface: ~6,500 lines deleted against ~2,500 added (net −4,000 lines in the Rust crate alone), with stronger cross-SDK behavioural parity.
Breaking changes for consumers
Every change below requires a downstream code edit. They are grouped by the symbol you'd grep for.
Handler API (the biggest churn)
trait SessionHandler(withon_event,on_permission_request,on_user_input,on_elicitation,on_external_tool,on_exit_plan_mode,on_auto_mode_switch,on_session_event)PermissionHandler::handle,ElicitationHandler::handle,UserInputHandler::handle,ExitPlanModeHandler::handle,AutoModeSwitchHandler::handle, plusToolHandler::call(already existed). Implement only the trait(s) you need.enum HandlerEventenum HandlerResponsePermissionResult,ElicitationResult,Option<UserInputResponse>,ExitPlanModeResult,AutoModeSwitchResponse,ToolResult).struct NoopHandlerNoneon the corresponding optional handler field (i.e. don't call thewith_*_handlerbuilder).struct ToolHandlerRouterSessionConfig::with_tool_handlers(Vec<Arc<dyn ToolHandler>>). The SDK builds the name→handler map internally. Duplicate tool names produceError::InvalidConfig.SessionConfig::with_handler(...)with_permission_handler,with_elicitation_handler,with_user_input_handler,with_exit_plan_mode_handler,with_auto_mode_switch_handler,with_tool_handlers.SessionHandler::on_session_event(catch-all observer)session.subscribe()→Stream<Item = SessionEvent>. Already part of the API; the trait method was redundant.ApproveAllHandlerandDenyAllHandlerstill exist but now implementPermissionHandler(one trait, one method), not the oldSessionHandler. Replacewith_handler(Arc::new(ApproveAllHandler))withwith_permission_handler(Arc::new(ApproveAllHandler)).SessionandClientmethod renamesSession::get_messages()Session::get_events()Vec<SessionEvent>, neverMessages.Session::destroy()Session::disconnect()destroy()retained with#[deprecated]for a transition window.ResumeSessionConfig::disable_resume_eventfieldResumeSessionConfig::suppress_resume_eventdisableResume) is unchanged.Type renames
InputOptions<'a>UiInputOptions<'a>The struct shape is identical; only the name changed. Update the import and the parameter type on
SessionUi::input.Type changes (field signatures)
McpStdioServerConfig::toolsVec<String>(with#[serde(default)])Option<Vec<String>>(tri-state)McpHttpServerConfig::toolsVec<String>(with#[serde(default)])Option<Vec<String>>(tri-state)ClientOptions::log_leveldefaultSome(LogLevel::Info)None(CLI default applies if not set)For the MCP
toolsfields:Nonemeans "expose all";Some(vec![])means "expose none";Some(vec!["a", "b"])means "expose only these".vec!["*"]is no longer the way to say "all" — omit the field instead.Wire-flag fields on
SessionConfig/ResumeSessionConfigThe user-facing
request_permission/request_elicitation/request_user_input/request_exit_plan_mode/request_auto_mode_switch/hooksfields are no longer caller-controllable knobs. They are derived from handler presence atClient::create_session/resume_sessiontime:PermissionHandler(or a permission policy) →requestPermission: trueon the wire.requestPermission: false.request_*flags.SessionHooksimpl viawith_hooks→hooks: trueon the wire.If your code was setting these fields directly to influence wire behaviour, remove those assignments and install (or don't install) the matching handler instead.
Permission policy ordering
with_permission_handler(...),approve_all_permissions(),deny_all_permissions(), andapprove_permissions_if(...)are now order-independent: calling them in any sequence produces the same effective handler. If your code relied on the old "later call overwrites earlier" semantics (e.g. to "clear" a wrap), it will behave differently — the policy and the handler now combine deterministically.What is NOT a breaking change
rust/src/generated/**are unchanged.rust 1.94.0).Design overview
Per-event handler traits (the headline change)
The old
SessionHandlermega-trait — one trait, eight default-impl methods, plus anon_event(HandlerEvent) -> HandlerResponsedispatcher — has been replaced with five focused, single-method, optional handler traits:PermissionHandlerhandle(session_id, request_id, data)PermissionResultElicitationHandlerhandle(session_id, request_id, request)ElicitationResultUserInputHandlerhandle(session_id, question, choices, allow_freeform)Option<UserInputResponse>ExitPlanModeHandlerhandle(session_id, data)ExitPlanModeResultAutoModeSwitchHandlerhandle(session_id, error_code, retry_after_seconds)AutoModeSwitchResponseEach is wired up via a dedicated builder on
SessionConfig/ResumeSessionConfig:The presence of a handler is the only signal. No
wants_*_dispatchprobes, no default no-op implementations that silently swallow events: if you didn't install a handler, the corresponding wire-flag isfalseand the SDK doesn't dispatch. This matches TS/C# semantics exactly — clients that don't install a permission handler are silently skipped when permission broadcasts arrive (allowing another connected client to handle them), instead of incorrectly auto-replying.ApproveAllHandler/DenyAllHandlerare nowPermissionHandlerimpls (one trait each, ~5 lines), used in tests and as built-in convenience handlers.Wire payload separated from user config
A new private
rust/src/wire.rsmodule definespub(crate) SessionCreateWire/SessionResumeWirestructs — the exact JSON shape sent on thesession.create/session.resumeRPCs.SessionConfig::to_wire()andResumeSessionConfig::to_wire()build them.Benefits:
SessionConfigno longer carries#[serde(skip)]on callback fields. Wire shape and user-API shape are decoupled.request_*wire flags are derived from handler presence atClient::create_session/resume_sessiontime. They are no longer caller-controllable knobs.hooksflag.Permission policy ordering bug fixed
Pre-PR, the order of
with_handler(...)andapprove_all_permissions()mattered —with_handlercould overwrite a previously-set policy and vice versa. Now both are stored as separatepermission_handlerandpermission_policyfields, andClient::create_sessioncomposes them viacrate::permission::resolve_handler(). Order-independent.Observability:
Session::subscribe(), not a callback methodRemoved
SessionHandler::on_session_event(which was the catch-all observation method). Consumers wanting to observe every event usesession.subscribe()— already present pre-PR — which yields atokio_stream::StreamoverSessionEventvalues. This is strictly more composable (works withselect!,filter,take,timeout) and avoids the silent-discard footgun of an un-overridden default trait method.Multi-client broadcast handling
The old default
on_permission_requestreturnedPermissionResult::Approved. In a multi-client deployment, every client connected to a session would auto-respond, causing duplicate / conflicting responses on the wire. The new design — optional handler fields, presence is the signal, missing handler → SDK doesn't respond — fixes this: another connected client gets to handle the request.Cross-SDK companion fix (included in this PR)
The runtime side (
copilot-agent-runtime) already supports presence-derivedrequestPermission:enablePermissionCallback: params.requestPermission ?? false. But every SDK was hardcodingrequestPermission: trueonsession.create, defeating the runtime's gating.Fixed in this PR for TS (
nodejs/src/client.ts), C# (dotnet/src/Client.cs), Go (go/client.go), and Python (python/copilot/client.py) — each now derives the flag from handler presence, matching the Rust behaviour. Without this, no SDK can correctly participate in a multi-client session.Items explicitly NOT in this PR
DateTime— kept asi64(epoch ms) to avoid pulling inchrono/time.SessionMetadatatimestamps as typedDateTime— same reason."local"alias removal —"local"is documented indocs/features/mcp.mdand standard in the wider MCP ecosystem.SessionConfig/ResumeSessionConfigshared base — the two structs still share ~30 duplicated fields and builders. Amacro_rules!-based extraction is feasible but invasive and doesn't change usability. Follow-up.SessionFsProvidermethod renames (mkdir→make_directory, etc.) — Go-specific; in Rust the short names mirrorstd::fs::create_dir/read_diretc.AsyncDisposableanalogue — Rust hasDrop(sync) and explicitstop()/force_stop(). There is noAsyncDrop.Testing
session_testintegration tests + 21 acrossjsonrpc_test/integration_test/protocol_version_test/api_types_testall pass locally.tests/e2e/*) compiles cleanly; runs against the live CLI in CI.test/scenarios/**/rust/build cleanly.cargo +nightly-2026-04-14 fmt --all -- --config-path .rustfmt.nightly.toml --checkclean.cargo clippy --all-targets --all-features -- -D warningsclean.cargo doc --no-deps --all-featuresclean (no broken intra-doc links, no redundant explicit links).Test migration summary
rust/tests/session_test.rs: +22 tests, −4 tests (net +18). The removed tests covered helpers, the now-deletedtool.calldirect-RPC arm, and an old assertion-shape test replaced bystop_is_safe_to_call. Behaviour coverage of every removed test lives on, either in an updated test or in a sibling test exercising the broadcast path.rust/tests/e2e/*: net 0 test count change. All migrated to the new API.rust/src/**inline tests: +5, −1.Known small coverage regression worth fixing before merge
stop_transitions_state_to_disconnected(which assertedclient.state() == ConnectionState::Disconnectedafterstop()) was reduced tostop_is_safe_to_call(juststop().await.expect(...)). The state-machine assertion is no longer covered. Easy to restore.Notes for reviewers
rust/src/generated/**(those remain owned by codegen).Co-authored-by: Copilot 223556219+Copilot@users.noreply.github.com