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
16 changes: 16 additions & 0 deletions internal/extension/codextool/customtool.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
38 changes: 38 additions & 0 deletions internal/extension/codextool/customtool_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
}
}
13 changes: 7 additions & 6 deletions internal/extension/codextool/tool_context.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
162 changes: 157 additions & 5 deletions internal/protocol/openai/adapter.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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{
Expand All @@ -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"
Expand Down Expand Up @@ -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",
}
Expand All @@ -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",
Expand All @@ -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",
}
}
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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
}