Skip to content

Commit 41c494d

Browse files
Update Go client
1 parent 2ee93b1 commit 41c494d

File tree

7 files changed

+609
-183
lines changed

7 files changed

+609
-183
lines changed

go/client.go

Lines changed: 10 additions & 99 deletions
Original file line numberDiff line numberDiff line change
@@ -948,6 +948,12 @@ func (c *Client) State() ConnectionState {
948948
return c.state
949949
}
950950

951+
// ActualPort returns the TCP port the CLI server is listening on.
952+
// Returns 0 if the client is not connected or using stdio transport.
953+
func (c *Client) ActualPort() int {
954+
return c.actualPort
955+
}
956+
951957
// Ping sends a ping request to the server to verify connectivity.
952958
//
953959
// The message parameter is optional and will be echoed back in the response.
@@ -1282,12 +1288,13 @@ func (c *Client) connectViaTcp(ctx context.Context) error {
12821288
return nil
12831289
}
12841290

1285-
// setupNotificationHandler configures handlers for session events, tool calls, and permission requests.
1291+
// setupNotificationHandler configures handlers for session events and RPC requests.
1292+
// Tool calls and permission requests are handled via the broadcast event model (protocol v3):
1293+
// the server broadcasts external_tool.requested / permission.requested as session events,
1294+
// and clients respond via session.tools.handlePendingToolCall / session.permissions.handlePendingPermissionRequest RPCs.
12861295
func (c *Client) setupNotificationHandler() {
12871296
c.client.SetRequestHandler("session.event", jsonrpc2.NotificationHandlerFor(c.handleSessionEvent))
12881297
c.client.SetRequestHandler("session.lifecycle", jsonrpc2.NotificationHandlerFor(c.handleLifecycleEvent))
1289-
c.client.SetRequestHandler("tool.call", jsonrpc2.RequestHandlerFor(c.handleToolCallRequest))
1290-
c.client.SetRequestHandler("permission.request", jsonrpc2.RequestHandlerFor(c.handlePermissionRequest))
12911298
c.client.SetRequestHandler("userInput.request", jsonrpc2.RequestHandlerFor(c.handleUserInputRequest))
12921299
c.client.SetRequestHandler("hooks.invoke", jsonrpc2.RequestHandlerFor(c.handleHooksInvoke))
12931300
}
@@ -1306,84 +1313,6 @@ func (c *Client) handleSessionEvent(req sessionEventRequest) {
13061313
}
13071314
}
13081315

1309-
// handleToolCallRequest handles a tool call request from the CLI server.
1310-
func (c *Client) handleToolCallRequest(req toolCallRequest) (*toolCallResponse, *jsonrpc2.Error) {
1311-
if req.SessionID == "" || req.ToolCallID == "" || req.ToolName == "" {
1312-
return nil, &jsonrpc2.Error{Code: -32602, Message: "invalid tool call payload"}
1313-
}
1314-
1315-
c.sessionsMux.Lock()
1316-
session, ok := c.sessions[req.SessionID]
1317-
c.sessionsMux.Unlock()
1318-
if !ok {
1319-
return nil, &jsonrpc2.Error{Code: -32602, Message: fmt.Sprintf("unknown session %s", req.SessionID)}
1320-
}
1321-
1322-
handler, ok := session.getToolHandler(req.ToolName)
1323-
if !ok {
1324-
return &toolCallResponse{Result: buildUnsupportedToolResult(req.ToolName)}, nil
1325-
}
1326-
1327-
result := c.executeToolCall(req.SessionID, req.ToolCallID, req.ToolName, req.Arguments, handler)
1328-
return &toolCallResponse{Result: result}, nil
1329-
}
1330-
1331-
// executeToolCall executes a tool handler and returns the result.
1332-
func (c *Client) executeToolCall(
1333-
sessionID, toolCallID, toolName string,
1334-
arguments any,
1335-
handler ToolHandler,
1336-
) (result ToolResult) {
1337-
invocation := ToolInvocation{
1338-
SessionID: sessionID,
1339-
ToolCallID: toolCallID,
1340-
ToolName: toolName,
1341-
Arguments: arguments,
1342-
}
1343-
1344-
defer func() {
1345-
if r := recover(); r != nil {
1346-
result = buildFailedToolResult(fmt.Sprintf("tool panic: %v", r))
1347-
}
1348-
}()
1349-
1350-
if handler != nil {
1351-
var err error
1352-
result, err = handler(invocation)
1353-
if err != nil {
1354-
result = buildFailedToolResult(err.Error())
1355-
}
1356-
}
1357-
1358-
return result
1359-
}
1360-
1361-
// handlePermissionRequest handles a permission request from the CLI server.
1362-
func (c *Client) handlePermissionRequest(req permissionRequestRequest) (*permissionRequestResponse, *jsonrpc2.Error) {
1363-
if req.SessionID == "" {
1364-
return nil, &jsonrpc2.Error{Code: -32602, Message: "invalid permission request payload"}
1365-
}
1366-
1367-
c.sessionsMux.Lock()
1368-
session, ok := c.sessions[req.SessionID]
1369-
c.sessionsMux.Unlock()
1370-
if !ok {
1371-
return nil, &jsonrpc2.Error{Code: -32602, Message: fmt.Sprintf("unknown session %s", req.SessionID)}
1372-
}
1373-
1374-
result, err := session.handlePermissionRequest(req.Request)
1375-
if err != nil {
1376-
// Return denial on error
1377-
return &permissionRequestResponse{
1378-
Result: PermissionRequestResult{
1379-
Kind: PermissionRequestResultKindDeniedCouldNotRequestFromUser,
1380-
},
1381-
}, nil
1382-
}
1383-
1384-
return &permissionRequestResponse{Result: result}, nil
1385-
}
1386-
13871316
// handleUserInputRequest handles a user input request from the CLI server.
13881317
func (c *Client) handleUserInputRequest(req userInputRequest) (*userInputResponse, *jsonrpc2.Error) {
13891318
if req.SessionID == "" || req.Question == "" {
@@ -1434,22 +1363,4 @@ func (c *Client) handleHooksInvoke(req hooksInvokeRequest) (map[string]any, *jso
14341363
return result, nil
14351364
}
14361365

1437-
// The detailed error is stored in the Error field but not exposed to the LLM for security.
1438-
func buildFailedToolResult(internalError string) ToolResult {
1439-
return ToolResult{
1440-
TextResultForLLM: "Invoking this tool produced an error. Detailed information is not available.",
1441-
ResultType: "failure",
1442-
Error: internalError,
1443-
ToolTelemetry: map[string]any{},
1444-
}
1445-
}
14461366

1447-
// buildUnsupportedToolResult creates a failure ToolResult for an unsupported tool.
1448-
func buildUnsupportedToolResult(toolName string) ToolResult {
1449-
return ToolResult{
1450-
TextResultForLLM: fmt.Sprintf("Tool '%s' is not supported by this client instance.", toolName),
1451-
ResultType: "failure",
1452-
Error: fmt.Sprintf("tool '%s' not supported", toolName),
1453-
ToolTelemetry: map[string]any{},
1454-
}
1455-
}

go/client_test.go

Lines changed: 0 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -12,41 +12,6 @@ import (
1212

1313
// This file is for unit tests. Where relevant, prefer to add e2e tests in e2e/*.test.go instead
1414

15-
func TestClient_HandleToolCallRequest(t *testing.T) {
16-
t.Run("returns a standardized failure result when a tool is not registered", func(t *testing.T) {
17-
cliPath := findCLIPathForTest()
18-
if cliPath == "" {
19-
t.Skip("CLI not found")
20-
}
21-
22-
client := NewClient(&ClientOptions{CLIPath: cliPath})
23-
t.Cleanup(func() { client.ForceStop() })
24-
25-
session, err := client.CreateSession(t.Context(), &SessionConfig{
26-
OnPermissionRequest: PermissionHandler.ApproveAll,
27-
})
28-
if err != nil {
29-
t.Fatalf("Failed to create session: %v", err)
30-
}
31-
32-
params := toolCallRequest{
33-
SessionID: session.SessionID,
34-
ToolCallID: "123",
35-
ToolName: "missing_tool",
36-
Arguments: map[string]any{},
37-
}
38-
response, _ := client.handleToolCallRequest(params)
39-
40-
if response.Result.ResultType != "failure" {
41-
t.Errorf("Expected resultType to be 'failure', got %q", response.Result.ResultType)
42-
}
43-
44-
if response.Result.Error != "tool 'missing_tool' not supported" {
45-
t.Errorf("Expected error to be \"tool 'missing_tool' not supported\", got %q", response.Result.Error)
46-
}
47-
})
48-
}
49-
5015
func TestClient_URLParsing(t *testing.T) {
5116
t.Run("should parse port-only URL format", func(t *testing.T) {
5217
client := NewClient(&ClientOptions{

0 commit comments

Comments
 (0)