Skip to content

Conversation

@ThomasK33
Copy link
Member

Add first-class support for ACP extension methods (JSON-RPC methods starting with _).

Key changes:

  • Add ExtensionMethodHandler for agent/client implementations to receive extension requests/notifications.
  • Route inbound _... methods through the handler (fallback to MethodNotFound per spec).
  • Add CallExtension/NotifyExtension helpers on both AgentSideConnection and ClientSideConnection.
  • Suppress noisy logs for unhandled extension notifications (MethodNotFound) to match spec.
  • Add unit tests + README docs.

Fixes #4

Tests:

  • go test ./...

📋 Implementation Plan

Plan: Support ACP extension methods (method names starting with _)

Context

  • GitHub issue: How to support method extension #4 requests a first-class way to handle ACP “extension methods” without intercepting raw stdio streams.
  • ACP spec: extension methods are JSON-RPC methods whose names must start with _ and are negotiated via _meta in capabilities. Unknown extension requests should return JSON-RPC “Method not found” (-32601); unknown extension notifications should be ignored.

Goals

  • Allow Agents and Clients built on this SDK to receive and dispatch extension method requests/notifications.
  • Provide an ergonomic way to send extension method requests/notifications from AgentSideConnection / ClientSideConnection.
  • Preserve backwards compatibility (no changes to acp.Agent / acp.Client interfaces).

Non-goals

  • Generate typed Go request/response structs for specific extensions (extensions remain user-defined).
  • Build a full capability negotiation framework (we’ll document how to use _meta, but not enforce a schema).

Proposed public API

1) Optional handler interface (non-breaking)

Add a new optional interface in a non-generated file (e.g. extensions.go):

// ExtensionMethodHandler can be implemented by either an Agent or a Client.
// It is invoked only for JSON-RPC methods whose names start with "_".
//
// If the method is unrecognized, return NewMethodNotFound(method).
// Return values are marshaled into the JSON-RPC result.
// Returned errors are converted via toReqErr (so *RequestError passes through).
//
// params is the raw JSON-RPC params payload.
// For notifications, the return value is ignored and errors are not sent to the peer.
//
// See: https://agentclientprotocol.com/protocol/extensibility#extension-methods
type ExtensionMethodHandler interface {
	HandleExtensionMethod(ctx context.Context, method string, params json.RawMessage) (any, error)
}

Why a single interface:

  • Works for both sides (agent receiving a client extension call; client receiving an agent extension call).
  • Avoids duplicating types (AgentExtensionHandler, ClientExtensionHandler) with identical signatures.

2) Outbound helpers on connections

Add additive helper methods:

func (c *AgentSideConnection) CallExtension[T any](ctx context.Context, method string, params any) (T, error)
func (c *AgentSideConnection) NotifyExtension(ctx context.Context, method string, params any) error

func (c *ClientSideConnection) CallExtension[T any](ctx context.Context, method string, params any) (T, error)
func (c *ClientSideConnection) NotifyExtension(ctx context.Context, method string, params any) error

Implementation notes:

  • Validate strings.HasPrefix(method, "_"); return a Go error if not.
  • Internally delegate to SendRequest[T] / Connection.SendNotification.
Alternative: expose the raw *Connection

Instead of adding helper methods, we could add Conn() *Connection to both wrapper types so callers can directly use SendRequest / SendNotification with arbitrary methods.

Pros: maximum flexibility (also helps with “future core methods” before regeneration).
Cons: leaks a lower-level type and makes the public surface bigger.

The helper-method approach keeps the abstraction and still covers the common use case.

Implementation steps

A) Dispatch inbound extension methods (agent + client)

  1. Add ExtensionMethodHandler interface (new file, non-generated).
  2. Update NewAgentSideConnection and NewClientSideConnection to pass a new wrapper handler to NewConnection:
    • In agent.go, replace NewConnection(asc.handle, …) with NewConnection(asc.handleWithExtensions, …).
    • In client.go, replace NewConnection(csc.handle, …) with NewConnection(csc.handleWithExtensions, …).
  3. Implement handleWithExtensions methods (non-generated) that:
    • If strings.HasPrefix(method, "_"):
      • If a.agent / c.client implements ExtensionMethodHandler, call it.
      • Convert returned error via toReqErr.
      • If no handler exists, return NewMethodNotFound(method).
    • Else: delegate to the generated handle method for core ACP methods.

This approach avoids modifying codegen and keeps the extension routing logic stable across regenerations.

B) Make notification behavior spec-friendly (reduce noise)

Currently, if a notification is unrecognized and the handler returns NewMethodNotFound, connection.go logs an error.

Adjust Connection.handleInbound notification path:

  • When req.ID == nil (notification) and the handler error is *RequestError with Code == -32601, do not log.
  • Preferably gate this to extension methods only (strings.HasPrefix(req.Method, "_")) so we keep visibility into unexpected core-method notifications.

C) Add outbound helpers for extension calls

Add CallExtension / NotifyExtension methods to both connection wrapper types.

  • Validate the method name starts with _.
  • Use existing lower-level helpers:
    • SendRequest[T](c.conn, ctx, method, params)
    • c.conn.SendNotification(ctx, method, params)

D) Tests

Extend acp_test.go with focused, table-driven tests:

  1. Client → Agent extension request dispatch
    • Make an agentFuncs-like struct that also implements ExtensionMethodHandler.
    • From the client side, call CallExtension with _vendor.test/echo.
    • Assert the handler was invoked with the correct method and raw params, and the response was decoded.
  2. Unknown extension request returns -32601
    • No handler installed; send an extension request; assert returned error is *RequestError{Code: -32601}.
  3. Extension notification does not log MethodNotFound
    • Send an unhandled extension notification.
    • Install a logger that writes to a buffer and assert the buffer does not contain the “failed to handle notification” log line.
  4. Agent → Client extension request dispatch
    • Mirror (1) but with the client implementing ExtensionMethodHandler.

E) Documentation / examples

  1. Update README.md with a new “Extension methods” section covering:
    • Naming (_vendor.id/feature/action convention).
    • Implementing ExtensionMethodHandler.
    • Advertising support via capabilities _meta (and calling side checking it).
  2. Add a small example snippet (README or example/) showing:
    • Agent advertises _meta capabilities.
    • Client calls _vendor.id/… extension method and handles response.
Notes on capability negotiation via _meta

The ACP spec uses _meta within capabilities as an extension point.
The SDK already models this as Meta any on AgentCapabilities / ClientCapabilities.

In docs, recommend:

  • Using a map[string]any or a small struct for _meta.
  • Avoiding reserved keys in _meta (traceparent, tracestate, baggage).
  • Treating negotiation as best-effort (feature detection), not as authorization.

Rollout

  • Backwards compatible: existing users do not need changes.
  • Ship as a minor version bump (new interfaces/methods only).
  • Mention in release notes that extension notifications with unknown methods are now ignored (no noisy logs) to match spec.

Generated with mux

Change-Id: Ida198f569e84f49da9908471a39998b741c283cf
Signed-off-by: Thomas Kosiewski <[email protected]>
Change-Id: I22441dd4cb63de24bd54ade48dc7d95b656897ea
Signed-off-by: Thomas Kosiewski <[email protected]>
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.

How to support method extension

1 participant