Skip to main content
This page documents the raw HTTP protocol for WarpGrep (morph-warp-grep-v2.1). Use it to build a custom harness in any language. The API follows the OpenAI chat completions format with native tool calling — you pass tool definitions, the model returns structured tool_calls, you execute them locally and send results back as tool messages. For a complete implementation, see the Python Guide or the Python agent example. For TypeScript SDK wrappers, see Agent Tool.

Message Flow

The agent runs a multi-turn conversation with max 6 turns using OpenAI-compatible tool calling:
user → assistant (tool_calls) → tool results → assistant (tool_calls) → ... → finish
StepRoleContent
1userRepo structure + search query
2assistanttool_calls array (structured JSON)
3toolOne message per tool call result
4+Repeat until finish is called

Initial User Message

The first user message contains two parts:
  1. Repository structure — flat list of absolute paths (depth 2)
  2. Search query — what the agent needs to find
<repo_structure>
/home/user/myproject
/home/user/myproject/README.md
/home/user/myproject/package.json
/home/user/myproject/src
/home/user/myproject/src/auth
/home/user/myproject/src/auth/login.py
/home/user/myproject/src/auth/session.py
/home/user/myproject/src/db
/home/user/myproject/src/utils
/home/user/myproject/tests
/home/user/myproject/config.py
/home/user/myproject/main.py
</repo_structure>

<search_string>
Find where user authentication is implemented
</search_string>
The repo structure must be flat absolute paths, one per line. First line is the repo root. No indentation, no tree characters. Directories have no trailing /.

API Call

The model has its tools built in — you do not need to pass a tools array. Just send the messages and the model returns structured tool_calls.
curl -X POST https://api.morphllm.com/v1/chat/completions \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "morph-warp-grep-v2.1",
    "messages": [
      {"role": "user", "content": "<repo_structure>\n...\n</repo_structure>\n\n<search_string>\nFind auth middleware\n</search_string>"}
    ],
    "temperature": 0.0,
    "max_tokens": 2048
  }'

Agent Response Format

The model responds with a standard OpenAI tool_calls array. No XML parsing needed.
{
  "id": "chatcmpl-abc123",
  "object": "chat.completion",
  "created": 1234567890,
  "model": "morph-warp-grep-v2.1",
  "choices": [{
    "index": 0,
    "message": {
      "role": "assistant",
      "content": "",
      "tool_calls": [
        {
          "id": "chatcmpl-tool-abc123",
          "type": "function",
          "function": {
            "name": "grep_search",
            "arguments": "{\"pattern\": \"(auth.*middleware|middleware.*auth)\", \"path\": \".\", \"glob\": \"*.{py,yml,json,yaml,y}\"}"
          }
        }
      ]
    },
    "finish_reason": "tool_calls"
  }],
  "usage": {
    "prompt_tokens": 1180,
    "total_tokens": 1245,
    "completion_tokens": 65
  }
}
The content field may contain non-empty text — ignore it and use only the tool_calls array. The finish_reason will be "tool_calls" when the model wants you to execute tools.
Execute each tool call locally and send results back as tool messages:
[
  {"role": "tool", "tool_call_id": "call_abc123", "content": "src/auth/login.py:45:def authenticate(username, password):"},
  {"role": "tool", "tool_call_id": "call_def456", "content": "login.py\nsession.py\nmiddleware/"}
]

Tool Definitions

The model calls these tools internally — you don’t need to pass them in the request. However, you need to implement each tool locally to execute the calls the model returns:
TOOLS = [
    {
        "type": "function",
        "function": {
            "name": "list_directory",
            "description": "Execute ls or find commands to explore directory structure. Max 500 results. Common junk directories are excluded automatically.",
            "parameters": {
                "type": "object",
                "properties": {
                    "command": {
                        "type": "string",
                        "description": "Full ls or find command (e.g. ls -la src/, find . -maxdepth 2 -type f -name '*.py', find . -type d, ls -d */)."
                    }
                },
                "required": ["command"]
            }
        }
    },
    {
        "type": "function",
        "function": {
            "name": "grep_search",
            "description": "Search for a regex pattern in file contents. Returns matching lines with file paths and line numbers. Case-insensitive. Respects .gitignore.",
            "parameters": {
                "type": "object",
                "properties": {
                    "pattern": {
                        "type": "string",
                        "description": "Regex pattern to search for in file contents (e.g. 'class\\s+\\w+Error', 'import|require|from', 'def (get|set|update)_user')."
                    },
                    "path": {
                        "type": "string",
                        "description": "File or directory to search in. Defaults to current working directory."
                    },
                    "glob": {
                        "type": "string",
                        "description": "Glob pattern to filter files (e.g. '*.py', '*.{ts,tsx,js,jsx,py,go}', 'src/**/*.go', '!*.test.*')."
                    },
                    "limit": {
                        "type": "integer",
                        "description": "Limit output to first N matching lines. Shows all matches if not specified."
                    }
                },
                "required": ["pattern"]
            }
        }
    },
    {
        "type": "function",
        "function": {
            "name": "glob",
            "description": "Find files by name/extension using glob patterns. Returns absolute paths sorted by modification time (newest first). Respects .gitignore. Max 100 results.",
            "parameters": {
                "type": "object",
                "properties": {
                    "pattern": {
                        "type": "string",
                        "description": "Glob pattern to match files (e.g. '*.py', 'src/**/*.js', '*.{ts,tsx}', 'test_*.py')."
                    },
                    "path": {
                        "type": "string",
                        "description": "Directory to search in. Defaults to repository root."
                    }
                },
                "required": ["pattern"]
            }
        }
    },
    {
        "type": "function",
        "function": {
            "name": "read",
            "description": "Read entire files or specific line ranges using absolute paths.",
            "parameters": {
                "type": "object",
                "properties": {
                    "path": {
                        "type": "string",
                        "description": "File path to read, using absolute path (e.g. '/home/ubuntu/repo/src/main.py' or windows path)."
                    },
                    "lines": {
                        "type": "string",
                        "description": "Optional line range (e.g. '1-50' or '1-20,45-80'). Omit to read entire file."
                    }
                },
                "required": ["path"]
            }
        }
    },
    {
        "type": "function",
        "function": {
            "name": "finish",
            "description": "Submit final answer with all relevant code locations. Include imports and over-include rather than miss context.",
            "parameters": {
                "type": "object",
                "properties": {
                    "files": {
                        "type": "string",
                        "description": "One file per line as path:lines (e.g. 'src/auth.py:1-15,25-50\\nsrc/user.py'). Omit line range to include entire file."
                    }
                },
                "required": ["files"]
            }
        }
    },
]

Executing Tools

When the model returns tool_calls, execute each one locally and return the output as a tool message. Here’s a minimal Python implementation:
import subprocess, os, glob as globmod

def execute_tool(name: str, args: dict, repo_root: str) -> str:
    if name == "grep_search":
        cmd = ["rg", "--line-number", "--no-heading", "--color=never", "-i", "-C", "1"]
        cmd.append(args["pattern"])
        cmd.append(args.get("path", repo_root))
        if "glob" in args:
            cmd.extend(["--glob", args["glob"]])
        if "limit" in args:
            cmd.extend(["--max-count", str(args["limit"])])
        result = subprocess.run(cmd, capture_output=True, text=True, cwd=repo_root)
        lines = result.stdout.strip().split("\n")
        return "\n".join(lines[:200])

    elif name == "read":
        path = args["path"] if os.path.isabs(args["path"]) else os.path.join(repo_root, args["path"])
        with open(path, "r") as f:
            all_lines = f.readlines()
        if "lines" in args:
            selected = []
            for part in args["lines"].split(","):
                start, end = map(int, part.split("-"))
                selected.extend(
                    f"{i}|{all_lines[i-1].rstrip()}"
                    for i in range(start, min(end + 1, len(all_lines) + 1))
                )
            return "\n".join(selected[:800])
        return "\n".join(f"{i+1}|{l.rstrip()}" for i, l in enumerate(all_lines[:800]))

    elif name == "list_directory":
        result = subprocess.run(
            args["command"], shell=True, capture_output=True, text=True, cwd=repo_root
        )
        return "\n".join(result.stdout.strip().split("\n")[:500])

    elif name == "glob":
        pattern = os.path.join(args.get("path", repo_root), "**", args["pattern"])
        matches = sorted(globmod.glob(pattern, recursive=True), key=os.path.getmtime, reverse=True)
        return "\n".join(matches[:100])

    elif name == "finish":
        output = []
        for spec in args["files"].strip().split("\n"):
            if ":" in spec and not spec.endswith(":*"):
                fpath, ranges = spec.rsplit(":", 1)
            else:
                fpath, ranges = spec.replace(":*", ""), None
            fpath = fpath if os.path.isabs(fpath) else os.path.join(repo_root, fpath)
            with open(fpath) as f:
                all_lines = f.readlines()
            if ranges:
                for part in ranges.split(","):
                    start, end = map(int, part.split("-"))
                    output.extend(all_lines[start - 1 : end])
            else:
                output.extend(all_lines)
        return "".join(output)

Turn Counter

After tool results, add a user message with a turn counter and context budget:
You have used 1 turn and have 5 remaining.
<context_budget>97% (525K/540K chars)</context_budget>
Turn messages by turn number:
TurnMessage
1You have used 1 turn and have 5 remaining
2You have used 2 turns and have 4 remaining
5You have used 5 turns, you only have 1 turn remaining. You have run out of turns to explore the code base and MUST call the finish tool now
If the model does not call finish within 6 turns, the search failed. Return an empty result to your caller.

Output Limits

Tools enforce output limits to prevent context explosion:
ToolMax LinesOn Exceed
grep_search200Truncate with warning
list_directory200Truncate with warning
read800Truncate with warning
glob100 filesTruncate

Complete Example

Putting it all together — a full agent loop:
import json
import openai

client = openai.OpenAI(base_url="https://api.morphllm.com/v1", api_key="YOUR_API_KEY")
repo_root = "/home/user/myapp"

messages = [
    {
        "role": "user",
        "content": (
            "<repo_structure>\n"
            "/home/user/myapp\n"
            "/home/user/myapp/src\n"
            "/home/user/myapp/src/auth\n"
            "/home/user/myapp/src/api\n"
            "/home/user/myapp/src/models\n"
            "/home/user/myapp/tests\n"
            "/home/user/myapp/package.json\n"
            "</repo_structure>\n\n"
            "<search_string>\nFind where JWT tokens are validated\n</search_string>"
        ),
    }
]

max_turns = 6
for turn in range(max_turns):
    response = client.chat.completions.create(
        model="morph-warp-grep-v2.1",
        messages=messages,
        temperature=0.0,
        max_tokens=2048,
    )
    msg = response.choices[0].message
    messages.append(msg)

    if not msg.tool_calls:
        break

    for tc in msg.tool_calls:
        args = json.loads(tc.function.arguments)
        result = execute_tool(tc.function.name, args, repo_root)

        if tc.function.name == "finish":
            # result contains the final file contents — done
            print(result)
            break

        messages.append({"role": "tool", "tool_call_id": tc.id, "content": result})
    else:
        # Add turn counter after all tool results
        remaining = max_turns - turn - 1
        turn_msg = f"You have used {turn + 1} turn{'s' if turn else ''} and have {remaining} remaining"
        if remaining <= 1:
            turn_msg += ". You have run out of turns to explore the code base and MUST call the finish tool now"
        messages.append({"role": "user", "content": turn_msg})
        continue
    break