Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions go/session.go
Original file line number Diff line number Diff line change
Expand Up @@ -1288,6 +1288,9 @@ func (s *Session) handleBroadcastEvent(event SessionEvent) {
case *ExternalToolRequestedData:
handler, ok := s.getToolHandler(d.ToolName)
if !ok {
if d.ToolName == "" && d.ToolCallID != "" {
s.respondToMissingToolName(d.RequestID, d.Traceparent, d.Tracestate)
}
return
}
var tp, ts string
Expand Down Expand Up @@ -1335,6 +1338,29 @@ func (s *Session) handleBroadcastEvent(event SessionEvent) {
}
}

func (s *Session) respondToMissingToolName(requestID string, traceparent, tracestate *string) {
var tp, ts string
if traceparent != nil {
tp = *traceparent
}
if tracestate != nil {
ts = *tracestate
}

ctx := contextWithTraceParent(context.Background(), tp, ts)
resultType := "failure"
errMsg := "tool name is missing or incorrect"
s.RPC.Tools.HandlePendingToolCall(ctx, &rpc.HandlePendingToolCallRequest{
RequestID: requestID,
Result: rpc.ExternalToolTextResultForLlm{
TextResultForLlm: "Tool call failed: tool name is missing or incorrect. Retry using one of the registered tool names.",
ResultType: &resultType,
Error: &errMsg,
ToolTelemetry: map[string]any{},
},
})
}

// executeToolAndRespond executes a tool handler and sends the result back via RPC.
func (s *Session) executeToolAndRespond(requestID, toolName, toolCallID string, arguments any, handler ToolHandler, traceparent, tracestate string) {
ctx := contextWithTraceParent(context.Background(), traceparent, tracestate)
Expand Down
96 changes: 96 additions & 0 deletions go/session_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,102 @@ func readTestJSONRPCFrame(r io.Reader) ([]byte, error) {
return data, err
}

func TestSession_HandleBroadcastEventRespondsToMissingToolName(t *testing.T) {
stdinR, stdinW := io.Pipe()
stdoutR, stdoutW := io.Pipe()
defer stdinR.Close()
defer stdinW.Close()
defer stdoutR.Close()
defer stdoutW.Close()

client := jsonrpc2.NewClient(stdinW, stdoutR)
client.Start()
defer client.Stop()

paramsCh := make(chan map[string]any, 1)
errCh := make(chan error, 1)

go func() {
frame, err := readTestJSONRPCFrame(stdinR)
if err != nil {
errCh <- err
return
}

var request struct {
ID json.RawMessage `json:"id"`
Method string `json:"method"`
Params map[string]any `json:"params"`
}
if err := json.Unmarshal(frame, &request); err != nil {
errCh <- err
return
}
if request.Method != "session.tools.handlePendingToolCall" {
errCh <- fmt.Errorf("expected session.tools.handlePendingToolCall, got %s", request.Method)
return
}

paramsCh <- request.Params

response := map[string]any{
"jsonrpc": "2.0",
"id": json.RawMessage(request.ID),
"result": map[string]any{"success": true},
}
data, err := json.Marshal(response)
if err != nil {
errCh <- err
return
}
if _, err := fmt.Fprintf(stdoutW, "Content-Length: %d\r\n\r\n%s", len(data), data); err != nil {
errCh <- err
return
}
}()

session := &Session{
SessionID: "sess-1",
client: client,
RPC: rpc.NewSessionRPC(client, "sess-1"),
}

session.handleBroadcastEvent(SessionEvent{Data: &ExternalToolRequestedData{
RequestID: "req-1",
SessionID: "sess-1",
ToolCallID: "call-1",
ToolName: "",
}})

select {
case params := <-paramsCh:
if params["sessionId"] != "sess-1" {
t.Fatalf("expected sessionId sess-1, got %v", params["sessionId"])
}
if params["requestId"] != "req-1" {
t.Fatalf("expected requestId req-1, got %v", params["requestId"])
}
result, ok := params["result"].(map[string]any)
if !ok {
t.Fatalf("expected structured result, got %T", params["result"])
}
if result["resultType"] != "failure" {
t.Fatalf("expected resultType failure, got %v", result["resultType"])
}
if result["error"] != "tool name is missing or incorrect" {
t.Fatalf("unexpected error: %v", result["error"])
}
text, ok := result["textResultForLlm"].(string)
if !ok || !strings.Contains(text, "tool name is missing") {
t.Fatalf("unexpected textResultForLlm: %v", result["textResultForLlm"])
}
case err := <-errCh:
t.Fatal(err)
case <-time.After(2 * time.Second):
t.Fatal("timed out waiting for pending tool call response")
}
}

func TestSession_On(t *testing.T) {
t.Run("multiple handlers all receive events", func(t *testing.T) {
session, cleanup := newTestSession()
Expand Down