Skip to content

Add v2 protocol backward compatibility adapters#706

Merged
SteveSandersonMS merged 3 commits intomainfrom
stevesa/protocol-v2-backcompat
Mar 7, 2026
Merged

Add v2 protocol backward compatibility adapters#706
SteveSandersonMS merged 3 commits intomainfrom
stevesa/protocol-v2-backcompat

Conversation

@SteveSandersonMS
Copy link
Contributor

@SteveSandersonMS SteveSandersonMS commented Mar 7, 2026

Summary

Adds runtime backward compatibility so SDK clients written against the v3 API still work when connected to a v2 CLI server.

Changes

  • Protocol version negotiation: Changed from strict equality to range-based [2, 3] across all 4 SDKs (Node.js, Python, Go, .NET). The negotiated version is stored on the client.
  • V2 adapter handlers: Always register tool.call and permission.request JSON-RPC request handlers on the connection. These adapters translate v2 server requests into calls to the same user-facing tool and permission handlers used by v3. V3 servers never send these requests, so the handlers are inert.

What's NOT included

  • No source-level backcompat — if type names changed between v2 and v3, users update their code.
  • The v2 server formats tool error content differently from v3. This is an inherent server-side difference that cannot be addressed in the adapter. It should not affect app behavior as the agent will still see the tool call failed either way.

Testing

Verified tools and permissions e2e tests pass across all 4 SDKs on both v3 (current CLI) and v2 (@github/copilot 0.0.420). The only known difference is the handles_tool_calling_errors test on v2 in Node/Go due to the server-side error formatting difference noted above.

@SteveSandersonMS SteveSandersonMS requested a review from a team as a code owner March 7, 2026 15:03
Copilot AI review requested due to automatic review settings March 7, 2026 15:03
Comment on lines +1336 to +1344
catch (Exception ex)
{
return new ToolCallResponseV2(new ToolResultObject
{
TextResultForLlm = "Invoking this tool produced an error. Detailed information is not available.",
ResultType = "failure",
Error = ex.Message
});
}
@github-actions
Copy link
Contributor

github-actions bot commented Mar 7, 2026

✅ Cross-SDK Consistency Review

I've reviewed this PR for consistency across all four SDK implementations (Node.js, Python, Go, and .NET). Excellent work on maintaining feature parity!

Summary

This PR adds v2 protocol backward compatibility adapters uniformly across all SDKs. The implementation is highly consistent, with appropriate language-specific adaptations.

Consistency Check ✅

Protocol Version Negotiation - All 4 SDKs implement:

  • ✅ Range-based version check (MIN_PROTOCOL_VERSION = 2, MAX_VERSION = 3)
  • ✅ Store negotiated version on the client
  • ✅ Consistent error messages for version mismatches
  • ✅ Appropriate naming: MIN_PROTOCOL_VERSION/MinProtocolVersion (Node/Python/Go/.NET)

V2 Adapter Handlers - All 4 SDKs implement:

  • tool.call RPC handler (handleToolCallRequestV2/_handle_tool_call_request_v2/handleToolCallRequestV2/OnToolCallV2)
  • permission.request RPC handler with parallel naming
  • ✅ Register handlers before version negotiation (inert on v3 servers)
  • ✅ Consistent error responses for missing tools/sessions

Error Handling - All SDKs return consistent error formats:

  • ✅ Missing tool: "Tool '{name}' is not supported by this client instance"
  • ✅ Tool exception: "Invoking this tool produced an error. Detailed information is not available."
  • ✅ Permission denied: kind: "denied-no-approval-rule-and-could-not-request-from-user"

Language-Specific Adaptations (appropriate differences):

  • ✅ Node.js: Additional normalizeToolResultV2 helper for type coercion (JS flexibility)
  • ✅ Python: inspect.isawaitable() check for sync/async handlers
  • ✅ Go: Explicit ok bool returns from getter methods (Go idiom)
  • ✅ .NET: AIFunctionArguments mapping + JSON serialization (Microsoft.Extensions.AI pattern)

Architecture Notes

The implementation correctly addresses the architectural difference between v2 and v3:

  • v3: Broadcast events (external_tool.requested, permission.requested) handled in session
  • v2: Direct RPC (tool.call, permission.request) handled in client

By registering both handler types and determining behavior based on the negotiated version, the SDKs maintain backward compatibility without code duplication.

Session-Level Support

Node.js added Session._handlePermissionRequestV2() in session.ts, while Python, Go, and .NET correctly reuse existing session-level permission handling methods:

  • Python: Session._handle_permission_request() (already existed)
  • Go: Session.getPermissionHandler() (already existed)
  • .NET: Session.HandlePermissionRequestAsync() (already existed)

This is appropriate—Node.js needed a dedicated v2 adapter, while other SDKs can delegate directly to existing session infrastructure.

No Consistency Issues Found 🎉

All SDKs implement the same feature set with parallel APIs and appropriate language conventions. The PR maintains excellent cross-SDK consistency!

Generated by SDK Consistency Review Agent for issue #706 ·

@SteveSandersonMS SteveSandersonMS force-pushed the stevesa/protocol-v2-backcompat branch from edf5896 to d15678c Compare March 7, 2026 15:10
Register v2-style tool.call and permission.request JSON-RPC request
handlers on all 4 SDKs so that SDK clients written against the v3 API
still work at runtime when connected to a v2 CLI server.

- Change protocol version negotiation from strict equality to range-based
  [MIN_PROTOCOL_VERSION(2), SDK_PROTOCOL_VERSION(3)]
- Always register v2 handlers unconditionally (v3 servers never send them)
- Node/Python/Go/.NET all implement handleToolCallRequestV2 and
  handlePermissionRequestV2 adapters that invoke the same user-facing
  tool and permission handlers

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@SteveSandersonMS SteveSandersonMS force-pushed the stevesa/protocol-v2-backcompat branch from d15678c to b8836bf Compare March 7, 2026 15:10
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adds runtime backward compatibility so SDK clients written against the v3 API can work when connected to a v2 CLI server, without requiring source-level changes from SDK users.

Changes:

  • Protocol version negotiation changed from strict equality to range-based [2, 3] across all 4 SDKs (Node.js, Python, Go, .NET), storing the negotiated version on the client for potential future use
  • v2 adapter handlers (tool.call and permission.request RPC request handlers) registered unconditionally on all 4 SDKs, translating v2 server requests into calls to the same user-facing tool/permission handlers used by v3
  • New internal types to support the v2 wire protocol response format in all 4 SDKs

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 8 comments.

Show a summary per file
File Description
python/copilot/client.py Range-based version negotiation, v2 adapter handlers registered in both stdio and TCP code paths, new _handle_tool_call_request_v2 and _handle_permission_request_v2 methods
nodejs/src/session.ts New _handlePermissionRequestV2 method on CopilotSession to delegate v2 permission requests to the existing permissionHandler
nodejs/src/client.ts Range-based version negotiation, v2 onRequest handler registrations, handleToolCallRequestV2, handlePermissionRequestV2, and normalizeToolResultV2 helper methods, plus new internal types
go/client.go Range-based version negotiation, v2 adapter handler registration, handleToolCallRequestV2 and handlePermissionRequestV2 methods, and new v2 request/response struct types
dotnet/src/Client.cs Range-based version negotiation (method changed to instance method), v2 RPC method registrations, OnToolCallV2 and OnPermissionRequestV2 on RpcHandler, new ToolCallResponseV2 and PermissionRequestResponseV2 record types, new JsonSerializable annotations

{
throw new InvalidOperationException(
$"SDK protocol version mismatch: SDK expects version {expectedVersion}, " +
$"SDK protocol version mismatch: SDK expects version {MinProtocolVersion}-{maxVersion}, " +
Copy link

Copilot AI Mar 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The error message when the server doesn't report a protocol version uses "SDK expects version 2-3", while the error message for an out-of-range version uses "SDK supports versions 2-3". The word "expects" is inconsistent with the range-based semantics. Both should use "SDK supports versions 2-3" for consistency.

Suggested change
$"SDK protocol version mismatch: SDK expects version {MinProtocolVersion}-{maxVersion}, " +
$"SDK protocol version mismatch: SDK supports versions {MinProtocolVersion}-{maxVersion}, " +

Copilot uses AI. Check for mistakes.

// Protocol v2 backward-compatibility response types
internal record ToolCallResponseV2(
ToolResultObject? Result);
Copy link

Copilot AI Mar 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The ToolCallResponseV2 record declares the Result property as ToolResultObject? (nullable). However, every code path in OnToolCallV2 always returns a non-null ToolResultObject. The nullable annotation is misleading. Using a non-nullable ToolResultObject Result would better express the intent that a result is always present, consistent with how PermissionRequestResponseV2 declares its PermissionRequestResult Result as non-nullable.

Suggested change
ToolResultObject? Result);
ToolResultObject Result);

Copilot uses AI. Check for mistakes.
Comment on lines +1349 to +1356
var session = client.GetSession(sessionId);
if (session == null)
{
return new PermissionRequestResponseV2(new PermissionRequestResult
{
Kind = PermissionRequestResultKind.DeniedCouldNotRequestFromUser
});
}
Copy link

Copilot AI Mar 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's an inconsistency between OnToolCallV2 and OnPermissionRequestV2 in their handling of an unknown sessionId. OnToolCallV2 throws ArgumentException (resulting in an RPC error response), while OnPermissionRequestV2 returns a graceful DeniedCouldNotRequestFromUser denial result for the same scenario. The Node.js and Python adapters both return an error for an unknown session in both cases. For consistency, OnPermissionRequestV2 should also throw (or return an error response) when the session is not found, rather than silently denying the request.

Suggested change
var session = client.GetSession(sessionId);
if (session == null)
{
return new PermissionRequestResponseV2(new PermissionRequestResult
{
Kind = PermissionRequestResultKind.DeniedCouldNotRequestFromUser
});
}
var session = client.GetSession(sessionId) ?? throw new ArgumentException($"Unknown session {sessionId}");

Copilot uses AI. Check for mistakes.
)

try:
import inspect
Copy link

Copilot AI Mar 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The import inspect statement on line 1574 is placed inside the function body (inside a try block), instead of at the top of the module. All other Python files in the codebase that use inspect import it at the top of the module (session.py:9, jsonrpc.py:9, tools.py:10). This deferred import is unnecessary and inconsistent — inspect is a standard library module and there's no reason to defer its import here. It should be moved to the top-level imports of client.py.

Copilot uses AI. Check for mistakes.
try:
perm_request = PermissionRequest.from_dict(permission_request)
result = await session._handle_permission_request(perm_request)
return {"result": {"kind": result.kind}}
Copy link

Copilot AI Mar 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Python _handle_permission_request_v2 method returns only {"result": {"kind": result.kind}}, dropping additional fields from PermissionRequestResult such as rules, feedback, message, and path. In contrast, the Go adapter (handlePermissionRequestV2) serializes the full PermissionRequestResult struct (including Kind and Rules) into the response. If the v2 server protocol uses these extra fields (e.g., rules), the Python adapter's response will be incomplete. The response object should include all fields from the result, not just kind, to be consistent with the Go adapter and fully convey the handler's decision.

Suggested change
return {"result": {"kind": result.kind}}
# Prefer full serialization of the permission request result when available.
if hasattr(result, "to_dict"):
result_payload = result.to_dict()
else:
# Fallback to the minimal payload to preserve existing behavior.
result_payload = {"kind": result.kind}
return {"result": result_payload}

Copilot uses AI. Check for mistakes.
go/client.go Outdated

if pingResult.ProtocolVersion == nil {
return fmt.Errorf("SDK protocol version mismatch: SDK expects version %d, but server does not report a protocol version. Please update your server to ensure compatibility", expectedVersion)
return fmt.Errorf("SDK protocol version mismatch: SDK expects version %d-%d, but server does not report a protocol version. Please update your server to ensure compatibility", minProtocolVersion, maxVersion)
Copy link

Copilot AI Mar 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The error message when the server doesn't report a protocol version uses "SDK expects version 2-3", while the error message for an out-of-range version uses "SDK supports versions 2-3". The word "expects" is inconsistent with the range-based semantics and the phrasing used in the other message. Both error messages should use "SDK supports versions 2-3" for consistency.

Suggested change
return fmt.Errorf("SDK protocol version mismatch: SDK expects version %d-%d, but server does not report a protocol version. Please update your server to ensure compatibility", minProtocolVersion, maxVersion)
return fmt.Errorf("SDK protocol version mismatch: SDK supports versions %d-%d, but server does not report a protocol version. Please update your server to ensure compatibility", minProtocolVersion, maxVersion)

Copilot uses AI. Check for mistakes.
f"but server does not report a protocol version. "
f"Please update your server to ensure compatibility."
"SDK protocol version mismatch: "
f"SDK expects version {MIN_PROTOCOL_VERSION}-{max_version}"
Copy link

Copilot AI Mar 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The error message when the server doesn't report a protocol version uses "SDK expects version 2-3", while the error message for an out-of-range version uses "SDK supports versions 2-3". The word "expects" is inconsistent with the range-based semantics and the phrasing used in the other message. Both should use "SDK supports versions 2-3" for consistency.

Suggested change
f"SDK expects version {MIN_PROTOCOL_VERSION}-{max_version}"
f"SDK supports versions {MIN_PROTOCOL_VERSION}-{max_version}"

Copilot uses AI. Check for mistakes.
if (serverVersion === undefined) {
throw new Error(
`SDK protocol version mismatch: SDK expects version ${expectedVersion}, but server does not report a protocol version. ` +
`SDK protocol version mismatch: SDK expects version ${MIN_PROTOCOL_VERSION}-${maxVersion}, but server does not report a protocol version. ` +
Copy link

Copilot AI Mar 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The error message when the server doesn't report a protocol version uses "SDK expects version 2-3", while the error message for an out-of-range version uses "SDK supports versions 2-3". The word "expects" is inconsistent with the range-based semantics. Both should use "SDK supports versions 2-3" for consistency.

Suggested change
`SDK protocol version mismatch: SDK expects version ${MIN_PROTOCOL_VERSION}-${maxVersion}, but server does not report a protocol version. ` +
`SDK protocol version mismatch: SDK supports versions ${MIN_PROTOCOL_VERSION}-${maxVersion}, but server does not report a protocol version. ` +

Copilot uses AI. Check for mistakes.
@github-actions
Copy link
Contributor

github-actions bot commented Mar 7, 2026

✅ Cross-SDK Consistency Review

I've reviewed this PR for consistency across all 4 SDK implementations (Node.js/TypeScript, Python, Go, and .NET). The changes maintain excellent cross-SDK consistency for the v2 protocol backward compatibility feature.

What Was Changed

This PR adds backward compatibility so SDK clients written against the v3 API can work with v2 CLI servers. The implementation is consistent across all 4 SDKs.

Consistency Verification

✅ Protocol Version Negotiation

All SDKs consistently implement range-based version negotiation:

  • Minimum version constant: MIN_PROTOCOL_VERSION = 2 / MinProtocolVersion = 2 / minProtocolVersion = 2
  • Version check: serverVersion >= MIN_VERSION && serverVersion <= MAX_VERSION
  • Stored negotiated version: negotiatedProtocolVersion (Node.js), _negotiated_protocol_version (Python), negotiatedProtocolVersion (Go), _negotiatedProtocolVersion (.NET)
  • Error messages: Consistent format across all SDKs

✅ V2 Adapter Handlers

All SDKs register the same v2 backward compatibility handlers:

  • tool.callhandleToolCallRequestV2 variants
  • permission.requesthandlePermissionRequestV2 variants

The registration happens unconditionally in all SDKs (before protocol negotiation), with consistent explanatory comments explaining that v3 servers will never invoke these handlers.

✅ Adapter Implementation Logic

The v2 adapter implementations follow the same pattern across all SDKs:

  1. Validate request payload (sessionId, toolCallId, toolName)
  2. Look up the session
  3. Invoke the appropriate handler (tool or permission)
  4. Return result in v2 format
  5. Handle errors with consistent fallback responses

✅ Comments and Documentation

All SDKs include consistent comments explaining:

  • Why v2 adapters are registered unconditionally
  • The difference between v2 (RPC) and v3 (broadcast events) models
  • That v3 servers never send these requests

Conclusion

No consistency issues found. This PR maintains excellent cross-SDK feature parity. All 4 language implementations:

  • Use the same protocol version range [2, 3]
  • Register the same v2 adapter handlers
  • Implement equivalent error handling
  • Include consistent documentation

The changes are well-coordinated and maintain the SDK's commitment to providing a consistent API experience across all supported languages.

Generated by SDK Consistency Review Agent for issue #706 ·

- Use 'supports versions' consistently in all error messages (all SDKs)
- Move 'import inspect' to top of module (Python)
- Return full PermissionRequestResult fields, not just kind (Python)
- Change bare catch to catch (Exception) (C#)
- Make ToolCallResponseV2.Result non-nullable (C#)
- Throw for unknown session in OnPermissionRequestV2 (C#)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Comment on lines +1357 to +1363
catch (Exception)
{
return new PermissionRequestResponseV2(new PermissionRequestResult
{
Kind = PermissionRequestResultKind.DeniedCouldNotRequestFromUser
});
}
@SteveSandersonMS SteveSandersonMS merged commit 396e8b3 into main Mar 7, 2026
33 checks passed
@SteveSandersonMS SteveSandersonMS deleted the stevesa/protocol-v2-backcompat branch March 7, 2026 15:25
@github-actions
Copy link
Contributor

github-actions bot commented Mar 7, 2026

✅ Cross-SDK Consistency Review

I've reviewed PR #706 for consistency across all four SDK implementations (Node.js, Python, Go, and .NET). Great work! The v2 backward compatibility feature has been implemented consistently across all SDKs.

Key Consistency Findings

Protocol Version Constants

All SDKs define the minimum protocol version as 2 with appropriate naming conventions:

  • Node.js: MIN_PROTOCOL_VERSION = 2
  • Python: MIN_PROTOCOL_VERSION = 2
  • Go: minProtocolVersion = 2 (unexported, as expected)
  • .NET: MinProtocolVersion = 2

Negotiated Version Storage

All SDKs store the negotiated protocol version in a nullable/optional integer field:

  • Node.js: private negotiatedProtocolVersion: number | null = null
  • Python: self._negotiated_protocol_version: int | None = None
  • Go: negotiatedProtocolVersion int (in Client struct)
  • .NET: private int? _negotiatedProtocolVersion

Range-Based Version Negotiation

All SDKs verify the server version is within [minVersion, maxVersion] range and store the negotiated version:

if (serverVersion < MIN_PROTOCOL_VERSION || serverVersion > maxVersion) {
    throw error
}
this.negotiatedProtocolVersion = serverVersion

V2 Adapter Handlers - Unconditional Registration

All SDKs unconditionally register tool.call and permission.request handlers before protocol version negotiation, with clear comments explaining that v3 servers will never invoke them:

  • Node.js (client.ts:1326-1343): Registers via connection.onRequest()
  • Python (client.py:1383-1388): Registers via set_request_handler()
  • Go (client.go:1305-1317): Registers via SetRequestHandler() in setupNotificationHandler()
  • .NET (Client.cs:1145-1157): Registers via AddLocalRpcMethod()

All include the same explanatory comment:

"Protocol v3 servers send tool calls and permission requests as broadcast events. Protocol v2 servers use the older tool.call / permission.request RPC model. We always register v2 adapters because handlers are set up before version negotiation; a v3 server will simply never send these requests."

Adapter Implementation Structure

All SDKs follow the same pattern:

  1. Receive v2-style RPC request with sessionId and request payload
  2. Look up the session
  3. Call the same user-facing handler used by v3 broadcast events
  4. Return v2-formatted response

Architectural Note (Not an Issue)

The v2 handler registration occurs at different layers in each SDK, reflecting language-specific design patterns:

  • Node.js: Session-level coordination
  • Python & Go: Client-level registration
  • .NET: RPC handler delegation

This is expected and appropriate — each SDK uses the architecture that's idiomatic for that language while maintaining functional consistency.

Conclusion

This PR successfully maintains feature parity and consistent API design across all four SDK implementations. The backward compatibility mechanism is implemented uniformly, with clear documentation and appropriate handling for both v2 and v3 protocol versions.


Note: There are separate code quality review comments from automated reviewers about exception handling, error message wording, and import placement. Those are orthogonal to cross-SDK consistency and should be addressed independently.

Generated by SDK Consistency Review Agent for issue #706 ·

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants