From 161497c3b756da4b39ec89276266d7bf73e003c5 Mon Sep 17 00:00:00 2001 From: Francesco Mosca Date: Wed, 3 Jun 2026 22:54:22 +0100 Subject: [PATCH 1/3] feat(codextool): introduce nested namespace tool kind and argument decoding Introduces the ToolNestedNamespace ToolKind to identify tools grouped under a shared namespace. Implements decodeNestedNamespaceArguments to reverse-map clean nested parameters back to Codex. --- internal/extension/codextool/customtool.go | 16 ++++++++++++++++ internal/extension/codextool/tool_context.go | 13 +++++++------ 2 files changed, 23 insertions(+), 6 deletions(-) diff --git a/internal/extension/codextool/customtool.go b/internal/extension/codextool/customtool.go index a83bf167..e3ea20ff 100644 --- a/internal/extension/codextool/customtool.go +++ b/internal/extension/codextool/customtool.go @@ -116,11 +116,27 @@ func OutputItemFromBlock( return "custom_tool_call", spec.OpenAIName, "", InputFromRaw(toolInput), false, nil case ToolFunction: return "function_call", spec.OpenAIName, spec.Namespace, string(toolInput), false, nil + case ToolNestedNamespace: + subName, paramsStr := decodeNestedNamespaceArguments(toolInput) + return "function_call", subName, blockName, paramsStr, false, nil default: return "function_call", blockName, "", string(toolInput), false, nil } } +type nestedCall struct { + Action string `json:"action"` + Params json.RawMessage `json:"params"` +} + +func decodeNestedNamespaceArguments(input json.RawMessage) (string, string) { + var call nestedCall + if err := json.Unmarshal(input, &call); err != nil { + return "", string(input) + } + return call.Action, string(call.Params) +} + // Proxy schema builders (return map[string]any for format.CoreTool.InputSchema) func ApplyPatchToolActions() []string { diff --git a/internal/extension/codextool/tool_context.go b/internal/extension/codextool/tool_context.go index cd8561a3..0cd31a51 100644 --- a/internal/extension/codextool/tool_context.go +++ b/internal/extension/codextool/tool_context.go @@ -11,12 +11,13 @@ package codextool type ToolKind string const ( - ToolApplyPatch ToolKind = "apply_patch" - ToolExec ToolKind = "exec" - ToolRaw ToolKind = "raw" - ToolFunction ToolKind = "function" - ToolLocalShell ToolKind = "local_shell" - ToolUnknown ToolKind = "unknown" + ToolApplyPatch ToolKind = "apply_patch" + ToolExec ToolKind = "exec" + ToolRaw ToolKind = "raw" + ToolFunction ToolKind = "function" + ToolLocalShell ToolKind = "local_shell" + ToolNestedNamespace ToolKind = "nested_namespace" + ToolUnknown ToolKind = "unknown" ) // ToolSpec describes an expanded tool entry for reverse mapping. From 1ca2b7ae88c9f9128f96dc1ef170adfee2121790 Mon Sep 17 00:00:00 2001 From: Francesco Mosca Date: Wed, 3 Jun 2026 22:54:25 +0100 Subject: [PATCH 2/3] feat(openai): implement nested namespace schemas and streaming translation Converts namespaced tool structures into a single top-level schema using unified action and anyOf-based parameters fields. Implements a stream buffering layer to reconstruct streamed action and parameter deltas into clean namespaced tool calls for the downstream interpreter. --- internal/protocol/openai/adapter.go | 162 +++++++++++++++++++++++++++- 1 file changed, 157 insertions(+), 5 deletions(-) diff --git a/internal/protocol/openai/adapter.go b/internal/protocol/openai/adapter.go index 32038518..f8149ea2 100644 --- a/internal/protocol/openai/adapter.go +++ b/internal/protocol/openai/adapter.go @@ -373,6 +373,12 @@ func (a *OpenAIAdapter) streamLoop(ctx context.Context, coreReq *format.CoreRequ reasonIndexes := make(map[int]bool) toolCallFinalized := make(map[int]bool) + type nestedBufferState struct { + toolName string + toolUseID string + } + nestedBuffers := make(map[int]*nestedBufferState) + for event := range events { // Let hooks skip events. if a.hooks.OnStreamEvent(ctx, event) { @@ -468,6 +474,18 @@ func (a *OpenAIAdapter) streamLoop(ctx context.Context, coreReq *format.CoreRequ } itemIDs[index] = fmt.Sprintf("fc_item_%d", index) toolBlockNames[index] = event.ContentBlock.ToolName + + // Check if this is a nested namespace tool + toolMap := codextool.DecodeToolMapFromExtensions(coreReq.Extensions) + if spec, ok := toolMap.Lookup(event.ContentBlock.ToolName); ok && spec.Kind == codextool.ToolNestedNamespace { + nestedBuffers[index] = &nestedBufferState{ + toolName: event.ContentBlock.ToolName, + toolUseID: toolUseID, + } + // Buffering: do NOT emit response.output_item.added yet + break + } + item := buildToolOutputItemStreaming(event.ContentBlock, coreReq.Extensions, toolUseID) outputIndexes[index] = len(response.Output) response.Output = append(response.Output, item) @@ -619,6 +637,12 @@ func (a *OpenAIAdapter) streamLoop(ctx context.Context, coreReq *format.CoreRequ case format.CoreToolCallArgsDelta: index := event.Index toolCallArgs[index] += event.Delta + + // If this is a buffered nested namespace call, we accumulate but do NOT stream delta to Codex + if _, isBuffered := nestedBuffers[index]; isBuffered { + break + } + send(StreamEvent{ Event: "response.function_call_arguments.delta", Data: FunctionCallArgumentsDeltaEvent{ @@ -642,6 +666,73 @@ func (a *OpenAIAdapter) streamLoop(ctx context.Context, coreReq *format.CoreRequ if finalArgs == "" { finalArgs = toolCallArgs[index] } + + // If this is a buffered nested namespace call, we resolve it now! + if nBuf, isBuffered := nestedBuffers[index]; isBuffered { + type nestedCall struct { + Action string `json:"action"` + Params json.RawMessage `json:"params"` + } + var call nestedCall + _ = json.Unmarshal([]byte(finalArgs), &call) + + // 1. Emit output_item.added with the clean action name as Name, and toolName as Namespace! + item := OutputItem{ + Type: "function_call", + ID: nBuf.toolUseID, + CallID: nBuf.toolUseID, + Name: call.Action, + Namespace: nBuf.toolName, + Status: "in_progress", + } + outIdx := len(response.Output) + outputIndexes[index] = outIdx + response.Output = append(response.Output, item) + + send(StreamEvent{ + Event: "response.output_item.added", + Data: OutputItemEvent{ + Type: "response.output_item.added", + SequenceNumber: next(), + OutputIndex: outIdx, + Item: item, + }, + }) + + // 2. Override finalArgs with the clean, unescaped params! + finalArgs = string(call.Params) + response.Output[outIdx].Arguments = finalArgs + response.Output[outIdx].Status = "completed" + + toolCallFinalized[index] = true + + // 3. Emit function_call_arguments.done + send(StreamEvent{ + Event: "response.function_call_arguments.done", + Data: FunctionCallArgumentsDoneEvent{ + Type: "response.function_call_arguments.done", + SequenceNumber: next(), + ItemID: itemIDs[index], + OutputIndex: outIdx, + Arguments: finalArgs, + }, + }) + + // 4. Emit output_item.done + send(StreamEvent{ + Event: "response.output_item.done", + Data: OutputItemEvent{ + Type: "response.output_item.done", + SequenceNumber: next(), + OutputIndex: outIdx, + Item: response.Output[outIdx], + }, + }) + + delete(nestedBuffers, index) + break + } + if idx, ok := outputIndexes[index]; ok && idx < len(response.Output) { response.Output[idx].Arguments = finalArgs response.Output[idx].Status = "completed" @@ -1477,13 +1568,17 @@ func buildToolOutputItem(block format.CoreContentBlock, extensions map[string]an Action: localShellActionFromRaw(actionJSON), } } + arguments := toolInputString(block.ToolInput) + if itemT == "function_call" && itemInput != "" && itemInput != string(block.ToolInput) { + arguments = itemInput + } return OutputItem{ Type: itemT, ID: block.ToolUseID, CallID: block.ToolUseID, Name: itemN, Namespace: itemNS, - Arguments: toolInputString(block.ToolInput), + Arguments: arguments, Input: itemInput, Status: "completed", } @@ -1494,7 +1589,6 @@ func buildToolOutputItem(block format.CoreContentBlock, extensions map[string]an func buildToolOutputItemStreaming(block *format.CoreContentBlock, extensions map[string]any, toolUseID string) OutputItem { toolMap := codextool.DecodeToolMapFromExtensions(extensions) itemT, itemN, itemNS, itemInput, isLS, actionJSON := codextool.OutputItemFromBlock(block.ToolName, block.ToolInput, toolMap) - _ = itemInput if isLS { return OutputItem{ Type: "local_shell_call", @@ -1504,13 +1598,17 @@ func buildToolOutputItemStreaming(block *format.CoreContentBlock, extensions map Action: localShellActionFromRaw(actionJSON), } } + arguments := toolInputString(block.ToolInput) + if itemT == "function_call" && itemInput != "" && itemInput != string(block.ToolInput) { + arguments = itemInput + } return OutputItem{ Type: itemT, ID: toolUseID, CallID: toolUseID, Name: itemN, Namespace: itemNS, - Arguments: toolInputString(block.ToolInput), + Arguments: arguments, Status: "in_progress", } } @@ -1572,8 +1670,7 @@ func convertToolWithNamespace(tool Tool, namespace string, disablePatchProxy fun }} case "namespace": - ns := namespacedToolName(namespace, tool.Name) - return flattenToolsWithNamespace(tool.Tools, ns, disablePatchProxy) + return []format.CoreTool{convertNamespaceToNestedTool(tool, namespace)} case "custom": grammar := codextool.CustomToolGrammar(tool.Format) @@ -1682,3 +1779,58 @@ func outputToContentBlocks(raw json.RawMessage) []format.CoreContentBlock { } return nil } + +func convertNamespaceToNestedTool(tool Tool, namespace string) format.CoreTool { + actionEnum := []string{} + anyOfSchemas := []map[string]any{} + + for _, sub := range tool.Tools { + actionEnum = append(actionEnum, sub.Name) + + // Each anyOf option represents one schema + subSchema := map[string]any{ + "type": "object", + "title": sub.Name + "_params", + "description": sub.Description, + } + if sub.Parameters != nil { + // Copy all fields from sub.Parameters + for k, v := range sub.Parameters { + if k != "title" && k != "description" { + subSchema[k] = v + } + } + } else { + subSchema["properties"] = map[string]any{} + } + anyOfSchemas = append(anyOfSchemas, subSchema) + } + + nestedSchema := map[string]any{ + "type": "object", + "required": []string{"action", "params"}, + "properties": map[string]any{ + "action": map[string]any{ + "type": "string", + "description": "The specific tool operation to perform within this namespace.", + "enum": actionEnum, + }, + "params": map[string]any{ + "type": "object", + "description": "Specific parameters corresponding to the selected action.", + "anyOf": anyOfSchemas, + }, + }, + } + + name := namespacedToolName(namespace, tool.Name) + ct := format.CoreTool{ + Name: name, + Description: tool.Description, + InputSchema: nestedSchema, + } + + codextool.AnnotateCoreTool(&ct, codextool.ToolNestedNamespace, name, "") + + return ct +} From 4ace9a6cc1a2db79dde5e225837ae1b6524f1638 Mon Sep 17 00:00:00 2001 From: Francesco Mosca Date: Wed, 3 Jun 2026 22:54:29 +0100 Subject: [PATCH 3/3] test(codextool): add unit test for nested namespace output reconstruction Adds TestOutputItemFromBlockForToolNestedNamespace to verify the extraction of clean action, namespace, and parameters when reconstructing output items. --- .../extension/codextool/customtool_test.go | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/internal/extension/codextool/customtool_test.go b/internal/extension/codextool/customtool_test.go index c1a55d2d..febe32c2 100644 --- a/internal/extension/codextool/customtool_test.go +++ b/internal/extension/codextool/customtool_test.go @@ -85,3 +85,41 @@ func TestRebuildGrammarUsesRawInputForGenericCustomTools(t *testing.T) { t.Fatalf("RebuildGrammar() = %q, want raw input", got) } } + +func TestOutputItemFromBlockForToolNestedNamespace(t *testing.T) { + toolMap := ToolMap{ + "mcp__filesystem": ToolSpec{ + Kind: ToolNestedNamespace, + OpenAIName: "mcp__filesystem", + }, + } + + input := json.RawMessage(`{ + "action": "read_file", + "params": { + "path": "/Users/francesco.mosca/Work/explorations/README.md" + } + }`) + + itemType, itemName, itemNamespace, toolInputStr, isLocalShell, _ := OutputItemFromBlock( + "mcp__filesystem", + input, + toolMap, + ) + + if itemType != "function_call" { + t.Errorf("expected itemType function_call, got %s", itemType) + } + if itemName != "read_file" { + t.Errorf("expected itemName read_file, got %s", itemName) + } + if itemNamespace != "mcp__filesystem" { + t.Errorf("expected itemNamespace mcp__filesystem, got %s", itemNamespace) + } + if !strings.Contains(toolInputStr, `"/Users/francesco.mosca/Work/explorations/README.md"`) { + t.Errorf("expected toolInputStr to contain path, got %s", toolInputStr) + } + if isLocalShell { + t.Errorf("expected isLocalShell false, got true") + } +}